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