github.com/codykaup/genqlient@v0.6.2/generate/generate_test.go (about) 1 package generate 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "strings" 10 "testing" 11 12 "github.com/codykaup/genqlient/internal/testutil" 13 "gopkg.in/yaml.v2" 14 ) 15 16 const ( 17 dataDir = "testdata/queries" 18 errorsDir = "testdata/errors" 19 ) 20 21 // buildGoFile returns an error if the given Go code is not valid. 22 // 23 // namePrefix is used for the temp-file, and is just for debugging. 24 func buildGoFile(namePrefix string, content []byte) error { 25 // We need to put this within the current module, rather than in 26 // /tmp, so that it can access internal/testutil. 27 f, err := os.CreateTemp("./testdata/tmp", namePrefix+"_*.go") 28 if err != nil { 29 return err 30 } 31 defer func() { 32 f.Close() 33 os.Remove(f.Name()) 34 }() 35 36 _, err = f.Write(content) 37 if err != nil { 38 return err 39 } 40 41 cmd := exec.Command("go", "build", f.Name()) 42 cmd.Stdout = os.Stdout 43 cmd.Stderr = os.Stderr 44 err = cmd.Run() 45 if err != nil { 46 return fmt.Errorf("generated code does not compile: %w", err) 47 } 48 return nil 49 } 50 51 // TestGenerate is a snapshot-based test of code-generation. 52 // 53 // This file just has the test runner; the actual data is all in 54 // testdata/queries. Specifically, the schema used for all the queries is in 55 // schema.graphql; the queries themselves are in TestName.graphql. The test 56 // asserts that running genqlient on that query produces the generated code in 57 // the snapshot-file TestName.graphql.go. 58 // 59 // To update the snapshots (if the code-generator has changed), run the test 60 // with `UPDATE_SNAPSHOTS=1`; it will fail the tests and print any diffs, but 61 // update the snapshots. Make sure to check that the output is sensible; the 62 // snapshots don't even get compiled! 63 func TestGenerate(t *testing.T) { 64 files, err := os.ReadDir(dataDir) 65 if err != nil { 66 t.Fatal(err) 67 } 68 69 for _, file := range files { 70 sourceFilename := file.Name() 71 if sourceFilename == "schema.graphql" || !strings.HasSuffix(sourceFilename, ".graphql") { 72 continue 73 } 74 goFilename := sourceFilename + ".go" 75 queriesFilename := sourceFilename + ".json" 76 77 t.Run(sourceFilename, func(t *testing.T) { 78 generated, err := Generate(&Config{ 79 Schema: []string{filepath.Join(dataDir, "schema.graphql")}, 80 Operations: []string{filepath.Join(dataDir, sourceFilename)}, 81 Package: "test", 82 Generated: goFilename, 83 ExportOperations: queriesFilename, 84 ContextType: "-", 85 Bindings: map[string]*TypeBinding{ 86 "ID": {Type: "github.com/codykaup/genqlient/internal/testutil.ID"}, 87 "DateTime": {Type: "time.Time"}, 88 "Date": { 89 Type: "time.Time", 90 Marshaler: "github.com/codykaup/genqlient/internal/testutil.MarshalDate", 91 Unmarshaler: "github.com/codykaup/genqlient/internal/testutil.UnmarshalDate", 92 }, 93 "Junk": {Type: "interface{}"}, 94 "ComplexJunk": {Type: "[]map[string]*[]*map[string]interface{}"}, 95 "Pokemon": { 96 Type: "github.com/codykaup/genqlient/internal/testutil.Pokemon", 97 ExpectExactFields: "{ species level }", 98 }, 99 "PokemonInput": {Type: "github.com/codykaup/genqlient/internal/testutil.Pokemon"}, 100 }, 101 AllowBrokenFeatures: true, 102 }) 103 if err != nil { 104 t.Fatal(err) 105 } 106 107 for filename, content := range generated { 108 t.Run(filename, func(t *testing.T) { 109 testutil.Cupaloy.SnapshotT(t, string(content)) 110 }) 111 } 112 113 t.Run("Build", func(t *testing.T) { 114 if testing.Short() { 115 t.Skip("skipping build due to -short") 116 } 117 118 err := buildGoFile(sourceFilename, generated[goFilename]) 119 if err != nil { 120 t.Error(err) 121 } 122 }) 123 }) 124 } 125 } 126 127 func getDefaultConfig(t *testing.T) *Config { 128 // Parse the config that `genqlient --init` generates, to make sure that 129 // works. 130 var config Config 131 b, err := os.ReadFile("default_genqlient.yaml") 132 if err != nil { 133 t.Fatal(err) 134 } 135 136 err = yaml.UnmarshalStrict(b, &config) 137 if err != nil { 138 t.Fatal(err) 139 } 140 return &config 141 } 142 143 // TestGenerateWithConfig tests several configuration options that affect 144 // generated code but don't require particular query structures to test. 145 // 146 // It runs a simple query from TestGenerate with several different genqlient 147 // configurations. It uses snapshots, just like TestGenerate. 148 func TestGenerateWithConfig(t *testing.T) { 149 tests := []struct { 150 name string 151 baseDir string // relative to dataDir 152 operations []string // overrides the default set below 153 config *Config // omits Schema and Operations, set below. 154 }{ 155 {"DefaultConfig", "", nil, getDefaultConfig(t)}, 156 {"Subpackage", "", nil, &Config{ 157 Generated: "mypkg/myfile.go", 158 }}, 159 {"SubpackageConfig", "mypkg", nil, &Config{ 160 Generated: "myfile.go", // (relative to genqlient.yaml) 161 }}, 162 {"PackageName", "", nil, &Config{ 163 Generated: "myfile.go", 164 Package: "mypkg", 165 }}, 166 {"ExportOperations", "", nil, &Config{ 167 ExportOperations: "operations.json", 168 }}, 169 {"CustomContext", "", nil, &Config{ 170 ContextType: "github.com/codykaup/genqlient/internal/testutil.MyContext", 171 }}, 172 {"CustomContextWithAlias", "", nil, &Config{ 173 ContextType: "github.com/codykaup/genqlient/internal/testutil/junk---fun.name.MyContext", 174 }}, 175 {"StructReferences", "", []string{"InputObject.graphql", "QueryWithStructs.graphql"}, &Config{ 176 StructReferences: true, 177 Bindings: map[string]*TypeBinding{ 178 "Date": { 179 Type: "time.Time", 180 Marshaler: "github.com/codykaup/genqlient/internal/testutil.MarshalDate", 181 Unmarshaler: "github.com/codykaup/genqlient/internal/testutil.UnmarshalDate", 182 }, 183 }, 184 }}, 185 {"StructReferencesAndOptionalPointer", "", []string{"InputObject.graphql", "QueryWithStructs.graphql"}, &Config{ 186 StructReferences: true, 187 Optional: "pointer", 188 Bindings: map[string]*TypeBinding{ 189 "Date": { 190 Type: "time.Time", 191 Marshaler: "github.com/codykaup/genqlient/internal/testutil.MarshalDate", 192 Unmarshaler: "github.com/codykaup/genqlient/internal/testutil.UnmarshalDate", 193 }, 194 }, 195 }}, 196 {"PackageBindings", "", nil, &Config{ 197 PackageBindings: []*PackageBinding{ 198 {Package: "github.com/codykaup/genqlient/internal/testutil"}, 199 }, 200 }}, 201 {"NoContext", "", nil, &Config{ 202 ContextType: "-", 203 }}, 204 {"ClientGetter", "", nil, &Config{ 205 ClientGetter: "github.com/codykaup/genqlient/internal/testutil.GetClientFromContext", 206 }}, 207 {"ClientGetterCustomContext", "", nil, &Config{ 208 ClientGetter: "github.com/codykaup/genqlient/internal/testutil.GetClientFromMyContext", 209 ContextType: "github.com/codykaup/genqlient/internal/testutil.MyContext", 210 }}, 211 {"ClientGetterNoContext", "", nil, &Config{ 212 ClientGetter: "github.com/codykaup/genqlient/internal/testutil.GetClientFromNowhere", 213 ContextType: "-", 214 }}, 215 {"Extensions", "", nil, &Config{ 216 Extensions: true, 217 }}, 218 {"OptionalValue", "", []string{"ListInput.graphql", "QueryWithSlices.graphql"}, &Config{ 219 Optional: "value", 220 }}, 221 {"OptionalPointer", "", []string{ 222 "ListInput.graphql", 223 "QueryWithSlices.graphql", 224 "SimpleQueryWithPointerFalseOverride.graphql", 225 "SimpleQueryNoOverride.graphql", 226 }, &Config{ 227 Optional: "pointer", 228 }}, 229 {"OptionalGeneric", "", []string{"ListInput.graphql", "QueryWithSlices.graphql"}, &Config{ 230 Optional: "generic", 231 OptionalGenericType: "github.com/codykaup/genqlient/internal/testutil.Option", 232 }}, 233 {"EnumRawCasingAll", "", []string{"QueryWithEnums.graphql"}, &Config{ 234 Casing: Casing{ 235 AllEnums: CasingRaw, 236 }, 237 }}, 238 {"EnumRawCasingSpecific", "", []string{"QueryWithEnums.graphql"}, &Config{ 239 Casing: Casing{ 240 Enums: map[string]CasingAlgorithm{"Role": CasingRaw}, 241 }, 242 }}, 243 } 244 245 sourceFilename := "SimpleQuery.graphql" 246 247 for _, test := range tests { 248 config := test.config 249 baseDir := filepath.Join(dataDir, test.baseDir) 250 t.Run(test.name, func(t *testing.T) { 251 err := config.ValidateAndFillDefaults(baseDir) 252 config.Schema = []string{filepath.Join(dataDir, "schema.graphql")} 253 if test.operations == nil { 254 config.Operations = []string{filepath.Join(dataDir, sourceFilename)} 255 } else { 256 config.Operations = make([]string, len(test.operations)) 257 for i := range test.operations { 258 config.Operations[i] = filepath.Join(dataDir, test.operations[i]) 259 } 260 } 261 if err != nil { 262 t.Fatal(err) 263 } 264 generated, err := Generate(config) 265 if err != nil { 266 t.Fatal(err) 267 } 268 269 for filename, content := range generated { 270 t.Run(filename, func(t *testing.T) { 271 testutil.Cupaloy.SnapshotT(t, string(content)) 272 }) 273 } 274 275 t.Run("Build", func(t *testing.T) { 276 if testing.Short() { 277 t.Skip("skipping build due to -short") 278 } 279 280 err := buildGoFile(sourceFilename, 281 generated[config.Generated]) 282 if err != nil { 283 t.Error(err) 284 } 285 }) 286 }) 287 } 288 } 289 290 // TestGenerateErrors is a snapshot-based test of error text. 291 // 292 // For each .go or .graphql file in testdata/errors, it asserts that the given 293 // query returns an error, and that that error's string-text matches the 294 // snapshot. The snapshotting is useful to ensure we don't accidentally make 295 // the text less readable, drop the line numbers, etc. We include both .go and 296 // .graphql tests for some of the test cases, to make sure the line numbers 297 // work in both cases. Tests may include a .schema.graphql file of their own, 298 // or use the shared schema.graphql in the same directory for convenience. 299 func TestGenerateErrors(t *testing.T) { 300 files, err := os.ReadDir(errorsDir) 301 if err != nil { 302 t.Fatal(err) 303 } 304 305 for _, file := range files { 306 sourceFilename := file.Name() 307 if !strings.HasSuffix(sourceFilename, ".graphql") && 308 !strings.HasSuffix(sourceFilename, ".go") || 309 strings.HasSuffix(sourceFilename, ".schema.graphql") || 310 sourceFilename == "schema.graphql" { 311 continue 312 } 313 314 baseFilename := strings.TrimSuffix(sourceFilename, filepath.Ext(sourceFilename)) 315 testFilename := strings.ReplaceAll(sourceFilename, ".", "/") 316 317 // Schema is either <base>.schema.graphql, or <dir>/schema.graphql if 318 // that doesn't exist. 319 schemaFilename := baseFilename + ".schema.graphql" 320 if _, err := os.Stat(filepath.Join(errorsDir, schemaFilename)); err != nil { 321 if errors.Is(err, os.ErrNotExist) { 322 schemaFilename = "schema.graphql" 323 } else { 324 t.Fatal(err) 325 } 326 } 327 328 t.Run(testFilename, func(t *testing.T) { 329 _, err := Generate(&Config{ 330 Schema: []string{filepath.Join(errorsDir, schemaFilename)}, 331 Operations: []string{filepath.Join(errorsDir, sourceFilename)}, 332 Package: "test", 333 Generated: os.DevNull, 334 ContextType: "context.Context", 335 Bindings: map[string]*TypeBinding{ 336 "ValidScalar": {Type: "string"}, 337 "InvalidScalar": {Type: "bogus"}, 338 "Pokemon": { 339 Type: "github.com/codykaup/genqlient/internal/testutil.Pokemon", 340 ExpectExactFields: "{ species level }", 341 }, 342 }, 343 AllowBrokenFeatures: true, 344 }) 345 if err == nil { 346 t.Fatal("expected an error") 347 } 348 349 testutil.Cupaloy.SnapshotT(t, err.Error()) 350 }) 351 } 352 }