|
| 1 | +package cli |
| 2 | + |
| 3 | +import ( |
| 4 | + "archive/tar" |
| 5 | + "bytes" |
| 6 | + "encoding/json" |
| 7 | + "errors" |
| 8 | + "io" |
| 9 | + "os" |
| 10 | + "path/filepath" |
| 11 | + "slices" |
| 12 | + "strings" |
| 13 | + "testing" |
| 14 | + |
| 15 | + "github.com/spf13/cobra" |
| 16 | +) |
| 17 | + |
| 18 | +// generate-docs documents the folder its positional argument names, else the |
| 19 | +// one --project-folder names; the working directory (the default ".") is left |
| 20 | +// alone either way. |
| 21 | +func TestGenerateDocsCommandsProjectFolder(t *testing.T) { |
| 22 | + t.Chdir(t.TempDir()) |
| 23 | + writeFeature(t, ".", "decoy", `{"id":"decoy","version":"1.0.0","name":"Decoy"}`) |
| 24 | + commands := map[string]struct { |
| 25 | + cmd func() *cobra.Command |
| 26 | + metadata string |
| 27 | + flags []string |
| 28 | + }{ |
| 29 | + "features": {realFeaturesGenerateDocsCmd, "devcontainer-feature.json", []string{"--namespace", "o/r"}}, |
| 30 | + "templates": {realTemplatesGenerateDocsCmd, "devcontainer-template.json", nil}, |
| 31 | + } |
| 32 | + for name, c := range commands { |
| 33 | + for _, positional := range []bool{true, false} { |
| 34 | + project := t.TempDir() |
| 35 | + if err := os.WriteFile(filepath.Join(project, c.metadata), []byte(`{"id":"solo","version":"1.0.0","name":"Solo"}`), 0o644); err != nil { |
| 36 | + t.Fatal(err) |
| 37 | + } |
| 38 | + args := []string{"--project-folder", project} |
| 39 | + if positional { |
| 40 | + args = []string{project} |
| 41 | + } |
| 42 | + cmd := c.cmd() |
| 43 | + cmd.SilenceUsage, cmd.SilenceErrors = true, true |
| 44 | + cmd.SetOut(&bytes.Buffer{}) |
| 45 | + cmd.SetErr(&bytes.Buffer{}) |
| 46 | + cmd.SetArgs(append(args, c.flags...)) |
| 47 | + if err := cmd.Execute(); err != nil { |
| 48 | + t.Fatalf("%s %v: %v", name, args, err) |
| 49 | + } |
| 50 | + if _, err := os.Stat(filepath.Join(project, "README.md")); err != nil { |
| 51 | + t.Errorf("%s %v: README not generated in the project folder: %v", name, args, err) |
| 52 | + } |
| 53 | + } |
| 54 | + } |
| 55 | + if _, err := os.Stat(filepath.Join("decoy", "README.md")); !errors.Is(err, os.ErrNotExist) { |
| 56 | + t.Errorf("the working directory was documented: %v", err) |
| 57 | + } |
| 58 | +} |
| 59 | + |
| 60 | +func runFeaturesTestCmd(t *testing.T, args ...string) (string, error) { |
| 61 | + t.Helper() |
| 62 | + cmd := realFeaturesTestCmd() |
| 63 | + cmd.SilenceUsage, cmd.SilenceErrors = true, true |
| 64 | + var out bytes.Buffer |
| 65 | + cmd.SetOut(&out) |
| 66 | + cmd.SetErr(&bytes.Buffer{}) |
| 67 | + cmd.SetArgs(args) |
| 68 | + err := cmd.Execute() |
| 69 | + return out.String(), err |
| 70 | +} |
| 71 | + |
| 72 | +// features test refuses the flag combinations TS refuses, before it looks at |
| 73 | +// the collection; each flag alone is accepted and reaches the collection check. |
| 74 | +func TestFeaturesTestCommandFlagConflicts(t *testing.T) { |
| 75 | + empty := t.TempDir() |
| 76 | + conflicts := map[string][]string{ |
| 77 | + "Cannot combine --global-scenarios-only and --features": {"--global-scenarios-only", "--features", "a"}, |
| 78 | + "Cannot combine --skip-scenarios and --global-scenarios-only": {"--global-scenarios-only", "--skip-scenarios"}, |
| 79 | + } |
| 80 | + for want, args := range conflicts { |
| 81 | + if _, err := runFeaturesTestCmd(t, append(args, "-p", empty)...); err == nil || err.Error() != want { |
| 82 | + t.Errorf("%v: err = %v, want %q", args, err, want) |
| 83 | + } |
| 84 | + } |
| 85 | + for _, args := range [][]string{{"--features", "a"}, {"--global-scenarios-only"}, {"--skip-scenarios"}} { |
| 86 | + if _, err := runFeaturesTestCmd(t, append(args, "-p", empty)...); exitCode(err) != 1 { |
| 87 | + t.Errorf("%v: err = %v, want the collection check's exit 1", args, err) |
| 88 | + } |
| 89 | + } |
| 90 | +} |
| 91 | + |
| 92 | +// features test checks the collection its positional target names, or |
| 93 | +// --project-folder's when the target is missing or "."; a collection without |
| 94 | +// src/ and test/ exits 1 naming the folder on stdout. |
| 95 | +func TestFeaturesTestCommandTarget(t *testing.T) { |
| 96 | + project, target := t.TempDir(), t.TempDir() |
| 97 | + cases := []struct { |
| 98 | + args []string |
| 99 | + want string |
| 100 | + }{ |
| 101 | + {[]string{target, "-p", project}, target}, |
| 102 | + {[]string{".", "-p", project}, project}, |
| 103 | + {[]string{"-p", project}, project}, |
| 104 | + } |
| 105 | + for _, c := range cases { |
| 106 | + out, err := runFeaturesTestCmd(t, c.args...) |
| 107 | + if exitCode(err) != 1 { |
| 108 | + t.Errorf("%v: err = %v, want exit 1", c.args, err) |
| 109 | + } |
| 110 | + if want := "[-] Folder '" + c.want + "' does not contain the required 'src' and 'test' folders.\n"; !strings.HasSuffix(out, want) { |
| 111 | + t.Errorf("%v: stdout = %q, want it to end with %q", c.args, out, want) |
| 112 | + } |
| 113 | + } |
| 114 | +} |
| 115 | + |
| 116 | +// orderedObjectKeys lists an object's own keys in source order, whatever its |
| 117 | +// values nest, and nothing for anything but an object. |
| 118 | +func TestOrderedObjectKeys(t *testing.T) { |
| 119 | + raw := `{"z":{"x":"y","k":{"deep":[1,{"q":"r"}]}},"a":["s",{"t":"u"}],"m":"v","b":null}` |
| 120 | + if got, want := orderedObjectKeys(json.RawMessage(raw)), []string{"z", "a", "m", "b"}; !slices.Equal(got, want) { |
| 121 | + t.Errorf("keys = %q, want %q", got, want) |
| 122 | + } |
| 123 | + for _, raw := range []string{``, `["a","b"]`, `"a"`, `{`} { |
| 124 | + if got := orderedObjectKeys(json.RawMessage(raw)); len(got) != 0 { |
| 125 | + t.Errorf("%s: keys = %q, want none", raw, got) |
| 126 | + } |
| 127 | + } |
| 128 | +} |
| 129 | + |
| 130 | +// createTarArchive roots the entries at "./", with forward slashes and a |
| 131 | +// trailing slash on directories, and records no user or group names, as |
| 132 | +// node-tar does. |
| 133 | +func TestCreateTarArchiveEntries(t *testing.T) { |
| 134 | + src := t.TempDir() |
| 135 | + if err := os.MkdirAll(filepath.Join(src, "sub"), 0o755); err != nil { |
| 136 | + t.Fatal(err) |
| 137 | + } |
| 138 | + if err := os.WriteFile(filepath.Join(src, "sub", "install.sh"), []byte("echo hi\n"), 0o644); err != nil { |
| 139 | + t.Fatal(err) |
| 140 | + } |
| 141 | + archive := filepath.Join(t.TempDir(), "out.tgz") |
| 142 | + if err := createTarArchive(archive, src); err != nil { |
| 143 | + t.Fatal(err) |
| 144 | + } |
| 145 | + f, err := os.Open(archive) |
| 146 | + if err != nil { |
| 147 | + t.Fatal(err) |
| 148 | + } |
| 149 | + defer f.Close() |
| 150 | + tr := tar.NewReader(f) |
| 151 | + var names []string |
| 152 | + for { |
| 153 | + h, err := tr.Next() |
| 154 | + if err == io.EOF { |
| 155 | + break |
| 156 | + } |
| 157 | + if err != nil { |
| 158 | + t.Fatal(err) |
| 159 | + } |
| 160 | + names = append(names, h.Name) |
| 161 | + if h.Uname != "" || h.Gname != "" { |
| 162 | + t.Errorf("%s: uname %q, gname %q, want both empty", h.Name, h.Uname, h.Gname) |
| 163 | + } |
| 164 | + if h.Name == "./sub/install.sh" { |
| 165 | + if b, _ := io.ReadAll(tr); string(b) != "echo hi\n" { |
| 166 | + t.Errorf("install.sh = %q", b) |
| 167 | + } |
| 168 | + } |
| 169 | + } |
| 170 | + if want := []string{"./", "./sub/", "./sub/install.sh"}; !slices.Equal(names, want) { |
| 171 | + t.Errorf("entries = %q, want %q", names, want) |
| 172 | + } |
| 173 | + // The archive is finished: whole 512-byte blocks ending in the two zero |
| 174 | + // blocks that mark the end of a tar. |
| 175 | + data, err := os.ReadFile(archive) |
| 176 | + if err != nil { |
| 177 | + t.Fatal(err) |
| 178 | + } |
| 179 | + if len(data)%512 != 0 || len(data) < 1024 || !bytes.Equal(data[len(data)-1024:], make([]byte, 1024)) { |
| 180 | + t.Errorf("archive of %d bytes does not end in the tar footer", len(data)) |
| 181 | + } |
| 182 | +} |
0 commit comments