github.com/snyk/vervet/v4@v4.27.2/internal/compiler/compiler_test.go (about) 1 package compiler 2 3 import ( 4 "bytes" 5 "context" 6 "io/fs" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "testing" 11 "text/template" 12 13 qt "github.com/frankban/quicktest" 14 15 "github.com/snyk/vervet/v4/config" 16 "github.com/snyk/vervet/v4/internal/files" 17 "github.com/snyk/vervet/v4/internal/linter" 18 "github.com/snyk/vervet/v4/testdata" 19 ) 20 21 func setup(c *qt.C) { 22 c.Setenv("API_BASE_URL", "https://example.com/api/rest") 23 cwd, err := os.Getwd() 24 c.Assert(err, qt.IsNil) 25 err = os.Chdir(testdata.Path("..")) 26 c.Assert(err, qt.IsNil) 27 c.Cleanup(func() { 28 err := os.Chdir(cwd) 29 c.Assert(err, qt.IsNil) 30 }) 31 } 32 33 var configTemplate = template.Must(template.New("vervet.yaml").Parse(` 34 linters: 35 resource-rules: 36 spectral: 37 rules: 38 - 'node_modules/@snyk/sweater-comb/resource.yaml' 39 compiled-rules: 40 spectral: 41 rules: 42 - 'node_modules/@snyk/sweater-comb/compiled.yaml' 43 apis: 44 rest-api: 45 resources: 46 - linter: resource-rules 47 path: 'testdata/resources' 48 excludes: 49 - 'testdata/resources/schemas/**' 50 overlays: 51 - include: 'testdata/resources/include.yaml' 52 - inline: |- 53 servers: 54 - url: ${API_BASE_URL} 55 description: Test REST API 56 output: 57 path: {{ . }} 58 linter: compiled-rules 59 `[1:])) 60 61 var configTemplateWithPaths = template.Must(template.New("vervet.yaml").Parse(` 62 linters: 63 resource-rules: 64 spectral: 65 rules: 66 - 'node_modules/@snyk/sweater-comb/resource.yaml' 67 compiled-rules: 68 spectral: 69 rules: 70 - 'node_modules/@snyk/sweater-comb/compiled.yaml' 71 apis: 72 rest-api: 73 resources: 74 - linter: resource-rules 75 path: 'testdata/resources' 76 excludes: 77 - 'testdata/resources/schemas/**' 78 overlays: 79 - include: 'testdata/resources/include.yaml' 80 - inline: |- 81 servers: 82 - url: ${API_BASE_URL} 83 description: Test REST API 84 output: 85 paths: 86 {{- range . }} 87 - {{ . }} 88 {{- end }} 89 linter: compiled-rules 90 `[1:])) 91 92 // Sanity-check the compiler at lifecycle stages in a simple scenario. This 93 // isn't meant to be a comprehensive end-to-end test of the compiler; those are 94 // done with fixtures. These are easier to break out, debug, and add specific 95 // asserts when the e2e fixtures fail. 96 func TestCompilerSmoke(t *testing.T) { 97 c := qt.New(t) 98 setup(c) 99 ctx := context.Background() 100 outputPath := c.TempDir() 101 var configBuf bytes.Buffer 102 err := configTemplate.Execute(&configBuf, outputPath) 103 c.Assert(err, qt.IsNil) 104 105 // Create a file that should be removed prior to build 106 err = ioutil.WriteFile(outputPath+"/goof", []byte("goof"), 0777) 107 c.Assert(err, qt.IsNil) 108 109 proj, err := config.Load(bytes.NewBuffer(configBuf.Bytes())) 110 c.Assert(err, qt.IsNil) 111 compiler, err := New(ctx, proj, LinterFactory(func(context.Context, *config.Linter) (linter.Linter, error) { 112 return &mockLinter{}, nil 113 })) 114 c.Assert(err, qt.IsNil) 115 116 // Assert constructor set things up as expected 117 c.Assert(compiler.apis, qt.HasLen, 1) 118 c.Assert(compiler.linters, qt.HasLen, 2) 119 restApi := compiler.apis["rest-api"] 120 c.Assert(restApi, qt.Not(qt.IsNil)) 121 c.Assert(restApi.resources, qt.HasLen, 1) 122 c.Assert(restApi.resources[0].sourceFiles, qt.Contains, "testdata/resources/projects/2021-06-04/spec.yaml") 123 c.Assert(restApi.overlayIncludes, qt.HasLen, 1) 124 c.Assert(restApi.overlayIncludes[0].Paths, qt.HasLen, 2) 125 c.Assert(restApi.overlayInlines[0].Servers[0].URL, qt.Contains, "https://example.com/api/rest", qt.Commentf("environment variable interpolation")) 126 c.Assert(restApi.output, qt.Not(qt.IsNil)) 127 128 // LintResources stage 129 err = compiler.LintResourcesAll(ctx) 130 c.Assert(err, qt.IsNil) 131 c.Assert(compiler.linters["resource-rules"].(*mockLinter).runs, qt.HasLen, 1) 132 c.Assert(compiler.linters["compiled-rules"].(*mockLinter).runs, qt.HasLen, 0) 133 c.Assert(compiler.linters["resource-rules"].(*mockLinter).runs[0], qt.Contains, "testdata/resources/projects/2021-06-04/spec.yaml") 134 135 // Build stage 136 err = compiler.BuildAll(ctx) 137 c.Assert(err, qt.IsNil) 138 139 // Verify created files/folders are as expected 140 // Look for existence of /2021-06-01~experimental 141 refOutputPath := testdata.Path("output") 142 assertOutputsEqual(c, refOutputPath, outputPath) 143 144 // Look for absence of /2021-06-01 folder (ga) 145 _, err = os.Stat(outputPath + "/2021-06-01") 146 c.Assert(os.IsNotExist(err), qt.IsTrue) 147 148 // Build output was cleaned up 149 _, err = ioutil.ReadFile(outputPath + "/goof") 150 c.Assert(err, qt.ErrorMatches, ".*/goof: no such file or directory") 151 152 // LintOutput stage 153 err = compiler.LintOutputAll(ctx) 154 c.Assert(err, qt.IsNil) 155 c.Assert(compiler.linters["resource-rules"].(*mockLinter).runs, qt.HasLen, 1) 156 c.Assert(compiler.linters["compiled-rules"].(*mockLinter).runs, qt.HasLen, 1) 157 c.Assert(compiler.linters["compiled-rules"].(*mockLinter).runs[0], qt.Contains, outputPath+"/2021-06-04~experimental/spec.yaml") 158 c.Assert(compiler.linters["compiled-rules"].(*mockLinter).runs[0], qt.Contains, outputPath+"/2021-06-04~experimental/spec.json") 159 } 160 161 func TestCompilerSmokePaths(t *testing.T) { 162 c := qt.New(t) 163 setup(c) 164 ctx := context.Background() 165 outputPaths := []string{c.TempDir(), c.TempDir()} 166 var configBuf bytes.Buffer 167 err := configTemplateWithPaths.Execute(&configBuf, outputPaths) 168 c.Assert(err, qt.IsNil) 169 170 // Create a file that should be removed prior to build 171 err = ioutil.WriteFile(outputPaths[0]+"/goof", []byte("goof"), 0777) 172 c.Assert(err, qt.IsNil) 173 174 proj, err := config.Load(bytes.NewBuffer(configBuf.Bytes())) 175 c.Assert(err, qt.IsNil) 176 compiler, err := New(ctx, proj, LinterFactory(func(context.Context, *config.Linter) (linter.Linter, error) { 177 return &mockLinter{}, nil 178 })) 179 c.Assert(err, qt.IsNil) 180 181 // Build stage 182 err = compiler.BuildAll(ctx) 183 c.Assert(err, qt.IsNil) 184 185 refOutputPath := testdata.Path("output") 186 // Verify created files/folders are as expected 187 for _, outputPath := range outputPaths { 188 assertOutputsEqual(c, refOutputPath, outputPath) 189 190 // Build output was cleaned up 191 _, err = ioutil.ReadFile(outputPath + "/goof") 192 c.Assert(err, qt.ErrorMatches, ".*/goof: no such file or directory") 193 } 194 195 // LintOutput stage 196 // Only the first output path is linted, others are copies 197 err = compiler.LintOutputAll(ctx) 198 c.Assert(err, qt.IsNil) 199 c.Assert(compiler.linters["resource-rules"].(*mockLinter).runs, qt.HasLen, 0) 200 c.Assert(compiler.linters["compiled-rules"].(*mockLinter).runs, qt.HasLen, 1) 201 c.Assert(compiler.linters["compiled-rules"].(*mockLinter).runs[0], qt.Contains, outputPaths[0]+"/2021-06-04~experimental/spec.yaml") 202 c.Assert(compiler.linters["compiled-rules"].(*mockLinter).runs[0], qt.Contains, outputPaths[0]+"/2021-06-04~experimental/spec.json") 203 } 204 205 func assertOutputsEqual(c *qt.C, refDir, testDir string) { 206 err := fs.WalkDir(os.DirFS(refDir), ".", func(path string, d fs.DirEntry, err error) error { 207 c.Assert(err, qt.IsNil) 208 if d.IsDir() { 209 return nil 210 } 211 if d.Name() != "spec.yaml" { 212 // only comparing compiled specs here 213 return nil 214 } 215 outputFile, err := os.ReadFile(filepath.Join(testDir, path)) 216 c.Assert(err, qt.IsNil) 217 refFile, err := os.ReadFile(filepath.Join(refDir, path)) 218 c.Assert(err, qt.IsNil) 219 c.Assert(string(outputFile), qt.Equals, string(refFile), qt.Commentf("%s", path)) 220 return nil 221 }) 222 c.Assert(err, qt.IsNil) 223 } 224 225 type mockLinter struct { 226 runs [][]string 227 override *config.Linter 228 err error 229 } 230 231 func (l *mockLinter) Match(rcConfig *config.ResourceSet) ([]string, error) { 232 return files.LocalFSSource{}.Match(rcConfig) 233 } 234 235 func (l *mockLinter) Run(ctx context.Context, root string, paths ...string) error { 236 l.runs = append(l.runs, paths) 237 return l.err 238 } 239 240 func (l *mockLinter) WithOverride(ctx context.Context, cfg *config.Linter) (linter.Linter, error) { 241 nl := &mockLinter{ 242 override: cfg, 243 } 244 return nl, nil 245 }