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  }