github.com/opiuman/genqlient@v1.0.0/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/opiuman/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/opiuman/genqlient/internal/testutil.ID"},
    87  					"DateTime": {Type: "time.Time"},
    88  					"Date": {
    89  						Type:        "time.Time",
    90  						Marshaler:   "github.com/opiuman/genqlient/internal/testutil.MarshalDate",
    91  						Unmarshaler: "github.com/opiuman/genqlient/internal/testutil.UnmarshalDate",
    92  					},
    93  					"Junk":        {Type: "interface{}"},
    94  					"ComplexJunk": {Type: "[]map[string]*[]*map[string]interface{}"},
    95  					"Pokemon": {
    96  						Type:              "github.com/opiuman/genqlient/internal/testutil.Pokemon",
    97  						ExpectExactFields: "{ species level }",
    98  					},
    99  					"PokemonInput": {Type: "github.com/opiuman/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  			Generated:        "generated.go",
   168  			ExportOperations: "operations.json",
   169  		}},
   170  		{"CustomContext", "", nil, &Config{
   171  			Generated:   "generated.go",
   172  			ContextType: "github.com/opiuman/genqlient/internal/testutil.MyContext",
   173  		}},
   174  		{"StructReferences", "", nil, &Config{
   175  			StructReferences: true,
   176  			Generated:        "generated-structrefs.go",
   177  		}},
   178  		{"NoContext", "", nil, &Config{
   179  			Generated:   "generated.go",
   180  			ContextType: "-",
   181  		}},
   182  		{"ClientGetter", "", nil, &Config{
   183  			Generated:    "generated.go",
   184  			ClientGetter: "github.com/opiuman/genqlient/internal/testutil.GetClientFromContext",
   185  		}},
   186  		{"ClientGetterCustomContext", "", nil, &Config{
   187  			Generated:    "generated.go",
   188  			ClientGetter: "github.com/opiuman/genqlient/internal/testutil.GetClientFromMyContext",
   189  			ContextType:  "github.com/opiuman/genqlient/internal/testutil.MyContext",
   190  		}},
   191  		{"ClientGetterNoContext", "", nil, &Config{
   192  			Generated:    "generated.go",
   193  			ClientGetter: "github.com/opiuman/genqlient/internal/testutil.GetClientFromNowhere",
   194  			ContextType:  "-",
   195  		}},
   196  		{"Extensions", "", nil, &Config{
   197  			Generated:  "generated.go",
   198  			Extensions: true,
   199  		}},
   200  		{"OptionalValue", "", []string{"ListInput.graphql", "QueryWithSlices.graphql"}, &Config{
   201  			Generated: "generated.go",
   202  			Optional:  "value",
   203  		}},
   204  		{"OptionalPointer", "", []string{"ListInput.graphql", "QueryWithSlices.graphql"}, &Config{
   205  			Generated: "generated.go",
   206  			Optional:  "pointer",
   207  		}},
   208  	}
   209  
   210  	sourceFilename := "SimpleQuery.graphql"
   211  
   212  	for _, test := range tests {
   213  		config := test.config
   214  		baseDir := filepath.Join(dataDir, test.baseDir)
   215  		t.Run(test.name, func(t *testing.T) {
   216  			err := config.ValidateAndFillDefaults(baseDir)
   217  			config.Schema = []string{filepath.Join(dataDir, "schema.graphql")}
   218  			if test.operations == nil {
   219  				config.Operations = []string{filepath.Join(dataDir, sourceFilename)}
   220  			} else {
   221  				config.Operations = make([]string, len(test.operations))
   222  				for i := range test.operations {
   223  					config.Operations[i] = filepath.Join(dataDir, test.operations[i])
   224  				}
   225  			}
   226  			if err != nil {
   227  				t.Fatal(err)
   228  			}
   229  			generated, err := Generate(config)
   230  			if err != nil {
   231  				t.Fatal(err)
   232  			}
   233  
   234  			for filename, content := range generated {
   235  				t.Run(filename, func(t *testing.T) {
   236  					testutil.Cupaloy.SnapshotT(t, string(content))
   237  				})
   238  			}
   239  
   240  			t.Run("Build", func(t *testing.T) {
   241  				if testing.Short() {
   242  					t.Skip("skipping build due to -short")
   243  				}
   244  
   245  				err := buildGoFile(sourceFilename,
   246  					generated[config.Generated])
   247  				if err != nil {
   248  					t.Error(err)
   249  				}
   250  			})
   251  		})
   252  	}
   253  }
   254  
   255  // TestGenerateErrors is a snapshot-based test of error text.
   256  //
   257  // For each .go or .graphql file in testdata/errors, it asserts that the given
   258  // query returns an error, and that that error's string-text matches the
   259  // snapshot.  The snapshotting is useful to ensure we don't accidentally make
   260  // the text less readable, drop the line numbers, etc.  We include both .go and
   261  // .graphql tests for some of the test cases, to make sure the line numbers
   262  // work in both cases.  Tests may include a .schema.graphql file of their own,
   263  // or use the shared schema.graphql in the same directory for convenience.
   264  func TestGenerateErrors(t *testing.T) {
   265  	files, err := os.ReadDir(errorsDir)
   266  	if err != nil {
   267  		t.Fatal(err)
   268  	}
   269  
   270  	for _, file := range files {
   271  		sourceFilename := file.Name()
   272  		if !strings.HasSuffix(sourceFilename, ".graphql") &&
   273  			!strings.HasSuffix(sourceFilename, ".go") ||
   274  			strings.HasSuffix(sourceFilename, ".schema.graphql") ||
   275  			sourceFilename == "schema.graphql" {
   276  			continue
   277  		}
   278  
   279  		baseFilename := strings.TrimSuffix(sourceFilename, filepath.Ext(sourceFilename))
   280  		testFilename := strings.ReplaceAll(sourceFilename, ".", "/")
   281  
   282  		// Schema is either <base>.schema.graphql, or <dir>/schema.graphql if
   283  		// that doesn't exist.
   284  		schemaFilename := baseFilename + ".schema.graphql"
   285  		if _, err := os.Stat(filepath.Join(errorsDir, schemaFilename)); err != nil {
   286  			if errors.Is(err, os.ErrNotExist) {
   287  				schemaFilename = "schema.graphql"
   288  			} else {
   289  				t.Fatal(err)
   290  			}
   291  		}
   292  
   293  		t.Run(testFilename, func(t *testing.T) {
   294  			_, err := Generate(&Config{
   295  				Schema:      []string{filepath.Join(errorsDir, schemaFilename)},
   296  				Operations:  []string{filepath.Join(errorsDir, sourceFilename)},
   297  				Package:     "test",
   298  				Generated:   os.DevNull,
   299  				ContextType: "context.Context",
   300  				Bindings: map[string]*TypeBinding{
   301  					"ValidScalar":   {Type: "string"},
   302  					"InvalidScalar": {Type: "bogus"},
   303  					"Pokemon": {
   304  						Type:              "github.com/opiuman/genqlient/internal/testutil.Pokemon",
   305  						ExpectExactFields: "{ species level }",
   306  					},
   307  				},
   308  				AllowBrokenFeatures: true,
   309  			})
   310  			if err == nil {
   311  				t.Fatal("expected an error")
   312  			}
   313  
   314  			testutil.Cupaloy.SnapshotT(t, err.Error())
   315  		})
   316  	}
   317  }