1// +build !windows
2// TODO(jen20): These need fixing on Windows but fmt is not used right now
3// and red CI is making it harder to process other bugs, so ignore until
4// we get around to fixing them.
5
6package fmtcmd
7
8import (
9	"bytes"
10	"fmt"
11	"io/ioutil"
12	"os"
13	"path/filepath"
14	"reflect"
15	"regexp"
16	"sort"
17	"syscall"
18	"testing"
19
20	"github.com/hashicorp/hcl/testhelper"
21)
22
23var fixtureExtensions = []string{"hcl"}
24
25func init() {
26	sort.Sort(ByFilename(fixtures))
27}
28
29func TestIsValidFile(t *testing.T) {
30	const fixtureDir = "./test-fixtures"
31
32	cases := []struct {
33		Path     string
34		Expected bool
35	}{
36		{"good.hcl", true},
37		{".hidden.ignore", false},
38		{"file.ignore", false},
39		{"dir.ignore", false},
40	}
41
42	for _, tc := range cases {
43		file, err := os.Stat(filepath.Join(fixtureDir, tc.Path))
44		if err != nil {
45			t.Errorf("unexpected error: %s", err)
46		}
47
48		if res := isValidFile(file, fixtureExtensions); res != tc.Expected {
49			t.Errorf("want: %t, got: %t", tc.Expected, res)
50		}
51	}
52}
53
54func TestRunMultiplePaths(t *testing.T) {
55	path1, err := renderFixtures("")
56	if err != nil {
57		t.Errorf("unexpected error: %s", err)
58	}
59	defer os.RemoveAll(path1)
60	path2, err := renderFixtures("")
61	if err != nil {
62		t.Errorf("unexpected error: %s", err)
63	}
64	defer os.RemoveAll(path2)
65
66	var expectedOut bytes.Buffer
67	for _, path := range []string{path1, path2} {
68		for _, fixture := range fixtures {
69			if !bytes.Equal(fixture.golden, fixture.input) {
70				expectedOut.WriteString(filepath.Join(path, fixture.filename) + "\n")
71			}
72		}
73	}
74
75	_, stdout := mockIO()
76	err = Run(
77		[]string{path1, path2},
78		fixtureExtensions,
79		nil, stdout,
80		Options{
81			List: true,
82		},
83	)
84
85	if err != nil {
86		t.Errorf("unexpected error: %s", err)
87	}
88	if stdout.String() != expectedOut.String() {
89		t.Errorf("stdout want:\n%s\ngot:\n%s", expectedOut.String(), stdout.String())
90	}
91}
92
93func TestRunSubDirectories(t *testing.T) {
94	pathParent, err := ioutil.TempDir("", "")
95	if err != nil {
96		t.Errorf("unexpected error: %s", err)
97	}
98	defer os.RemoveAll(pathParent)
99
100	path1, err := renderFixtures(pathParent)
101	if err != nil {
102		t.Errorf("unexpected error: %s", err)
103	}
104	path2, err := renderFixtures(pathParent)
105	if err != nil {
106		t.Errorf("unexpected error: %s", err)
107	}
108
109	paths := []string{path1, path2}
110	sort.Strings(paths)
111
112	var expectedOut bytes.Buffer
113	for _, path := range paths {
114		for _, fixture := range fixtures {
115			if !bytes.Equal(fixture.golden, fixture.input) {
116				expectedOut.WriteString(filepath.Join(path, fixture.filename) + "\n")
117			}
118		}
119	}
120
121	_, stdout := mockIO()
122	err = Run(
123		[]string{pathParent},
124		fixtureExtensions,
125		nil, stdout,
126		Options{
127			List: true,
128		},
129	)
130
131	if err != nil {
132		t.Errorf("unexpected error: %s", err)
133	}
134	if stdout.String() != expectedOut.String() {
135		t.Errorf("stdout want:\n%s\ngot:\n%s", expectedOut.String(), stdout.String())
136	}
137}
138
139func TestRunStdin(t *testing.T) {
140	var expectedOut bytes.Buffer
141	for i, fixture := range fixtures {
142		if i != 0 {
143			expectedOut.WriteString("\n")
144		}
145		expectedOut.Write(fixture.golden)
146	}
147
148	stdin, stdout := mockIO()
149	for _, fixture := range fixtures {
150		stdin.Write(fixture.input)
151	}
152
153	err := Run(
154		[]string{},
155		fixtureExtensions,
156		stdin, stdout,
157		Options{},
158	)
159
160	if err != nil {
161		t.Errorf("unexpected error: %s", err)
162	}
163	if !bytes.Equal(stdout.Bytes(), expectedOut.Bytes()) {
164		t.Errorf("stdout want:\n%s\ngot:\n%s", expectedOut.String(), stdout.String())
165	}
166}
167
168func TestRunStdinAndWrite(t *testing.T) {
169	var expectedOut = []byte{}
170
171	stdin, stdout := mockIO()
172	stdin.WriteString("")
173	err := Run(
174		[]string{}, []string{},
175		stdin, stdout,
176		Options{
177			Write: true,
178		},
179	)
180
181	if err != ErrWriteStdin {
182		t.Errorf("error want:\n%s\ngot:\n%s", ErrWriteStdin, err)
183	}
184	if !bytes.Equal(stdout.Bytes(), expectedOut) {
185		t.Errorf("stdout want:\n%s\ngot:\n%s", expectedOut, stdout)
186	}
187}
188
189func TestRunFileError(t *testing.T) {
190	path, err := ioutil.TempDir("", "")
191	if err != nil {
192		t.Errorf("unexpected error: %s", err)
193	}
194	defer os.RemoveAll(path)
195	filename := filepath.Join(path, "unreadable.hcl")
196
197	var expectedError = &os.PathError{
198		Op:   "open",
199		Path: filename,
200		Err:  syscall.EACCES,
201	}
202
203	err = ioutil.WriteFile(filename, []byte{}, 0000)
204	if err != nil {
205		t.Errorf("unexpected error: %s", err)
206	}
207
208	_, stdout := mockIO()
209	err = Run(
210		[]string{path},
211		fixtureExtensions,
212		nil, stdout,
213		Options{},
214	)
215
216	if !reflect.DeepEqual(err, expectedError) {
217		t.Errorf("error want: %#v, got: %#v", expectedError, err)
218	}
219}
220
221func TestRunNoOptions(t *testing.T) {
222	path, err := renderFixtures("")
223	if err != nil {
224		t.Errorf("unexpected error: %s", err)
225	}
226	defer os.RemoveAll(path)
227
228	var expectedOut bytes.Buffer
229	for _, fixture := range fixtures {
230		expectedOut.Write(fixture.golden)
231	}
232
233	_, stdout := mockIO()
234	err = Run(
235		[]string{path},
236		fixtureExtensions,
237		nil, stdout,
238		Options{},
239	)
240
241	if err != nil {
242		t.Errorf("unexpected error: %s", err)
243	}
244	if stdout.String() != expectedOut.String() {
245		t.Errorf("stdout want:\n%s\ngot:\n%s", expectedOut.String(), stdout.String())
246	}
247}
248
249func TestRunList(t *testing.T) {
250	path, err := renderFixtures("")
251	if err != nil {
252		t.Errorf("unexpected error: %s", err)
253	}
254	defer os.RemoveAll(path)
255
256	var expectedOut bytes.Buffer
257	for _, fixture := range fixtures {
258		if !bytes.Equal(fixture.golden, fixture.input) {
259			expectedOut.WriteString(fmt.Sprintln(filepath.Join(path, fixture.filename)))
260		}
261	}
262
263	_, stdout := mockIO()
264	err = Run(
265		[]string{path},
266		fixtureExtensions,
267		nil, stdout,
268		Options{
269			List: true,
270		},
271	)
272
273	if err != nil {
274		t.Errorf("unexpected error: %s", err)
275	}
276	if stdout.String() != expectedOut.String() {
277		t.Errorf("stdout want:\n%s\ngot:\n%s", expectedOut.String(), stdout.String())
278	}
279}
280
281func TestRunWrite(t *testing.T) {
282	path, err := renderFixtures("")
283	if err != nil {
284		t.Errorf("unexpected error: %s", err)
285	}
286	defer os.RemoveAll(path)
287
288	_, stdout := mockIO()
289	err = Run(
290		[]string{path},
291		fixtureExtensions,
292		nil, stdout,
293		Options{
294			Write: true,
295		},
296	)
297
298	if err != nil {
299		t.Errorf("unexpected error: %s", err)
300	}
301	for _, fixture := range fixtures {
302		res, err := ioutil.ReadFile(filepath.Join(path, fixture.filename))
303		if err != nil {
304			t.Errorf("unexpected error: %s", err)
305		}
306		if !bytes.Equal(res, fixture.golden) {
307			t.Errorf("file %q contents want:\n%s\ngot:\n%s", fixture.filename, fixture.golden, res)
308		}
309	}
310}
311
312func TestRunDiff(t *testing.T) {
313	path, err := renderFixtures("")
314	if err != nil {
315		t.Errorf("unexpected error: %s", err)
316	}
317	defer os.RemoveAll(path)
318
319	var expectedOut bytes.Buffer
320	for _, fixture := range fixtures {
321		if len(fixture.diff) > 0 {
322			expectedOut.WriteString(
323				regexp.QuoteMeta(
324					fmt.Sprintf("diff a/%s/%s b/%s/%s\n", path, fixture.filename, path, fixture.filename),
325				),
326			)
327			// Need to use regex to ignore datetimes in diff.
328			expectedOut.WriteString(`--- .+?\n`)
329			expectedOut.WriteString(`\+\+\+ .+?\n`)
330			expectedOut.WriteString(regexp.QuoteMeta(string(fixture.diff)))
331		}
332	}
333
334	expectedOutString := testhelper.Unix2dos(expectedOut.String())
335
336	_, stdout := mockIO()
337	err = Run(
338		[]string{path},
339		fixtureExtensions,
340		nil, stdout,
341		Options{
342			Diff: true,
343		},
344	)
345
346	if err != nil {
347		t.Errorf("unexpected error: %s", err)
348	}
349	if !regexp.MustCompile(expectedOutString).Match(stdout.Bytes()) {
350		t.Errorf("stdout want match:\n%s\ngot:\n%q", expectedOutString, stdout)
351	}
352}
353
354func mockIO() (stdin, stdout *bytes.Buffer) {
355	return new(bytes.Buffer), new(bytes.Buffer)
356}
357
358type fixture struct {
359	filename            string
360	input, golden, diff []byte
361}
362
363type ByFilename []fixture
364
365func (s ByFilename) Len() int           { return len(s) }
366func (s ByFilename) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }
367func (s ByFilename) Less(i, j int) bool { return len(s[i].filename) > len(s[j].filename) }
368
369var fixtures = []fixture{
370	{
371		"noop.hcl",
372		[]byte(`resource "aws_security_group" "firewall" {
373  count = 5
374}
375`),
376		[]byte(`resource "aws_security_group" "firewall" {
377  count = 5
378}
379`),
380		[]byte(``),
381	}, {
382		"align_equals.hcl",
383		[]byte(`variable "foo" {
384  default = "bar"
385  description = "bar"
386}
387`),
388		[]byte(`variable "foo" {
389  default     = "bar"
390  description = "bar"
391}
392`),
393		[]byte(`@@ -1,4 +1,4 @@
394 variable "foo" {
395-  default = "bar"
396+  default     = "bar"
397   description = "bar"
398 }
399`),
400	}, {
401		"indentation.hcl",
402		[]byte(`provider "aws" {
403    access_key = "foo"
404    secret_key = "bar"
405}
406`),
407		[]byte(`provider "aws" {
408  access_key = "foo"
409  secret_key = "bar"
410}
411`),
412		[]byte(`@@ -1,4 +1,4 @@
413 provider "aws" {
414-    access_key = "foo"
415-    secret_key = "bar"
416+  access_key = "foo"
417+  secret_key = "bar"
418 }
419`),
420	},
421}
422
423// parent can be an empty string, in which case the system's default
424// temporary directory will be used.
425func renderFixtures(parent string) (path string, err error) {
426	path, err = ioutil.TempDir(parent, "")
427	if err != nil {
428		return "", err
429	}
430
431	for _, fixture := range fixtures {
432		err = ioutil.WriteFile(filepath.Join(path, fixture.filename), []byte(fixture.input), 0644)
433		if err != nil {
434			os.RemoveAll(path)
435			return "", err
436		}
437	}
438
439	return path, nil
440}
441