| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // The following enables go generate to generate the doc.go file. |
| //go:generate go run $JIRI_ROOT/release/go/src/v.io/x/lib/cmdline/testdata/gendoc.go . -help |
| |
| package main |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "strings" |
| |
| "v.io/v23/vdl" |
| "v.io/v23/vom" |
| "v.io/x/lib/cmdline" |
| "v.io/x/ref/lib/vdl/build" |
| "v.io/x/ref/lib/vdl/codegen" |
| "v.io/x/ref/lib/vdl/codegen/vdlgen" |
| "v.io/x/ref/lib/vdl/compile" |
| _ "v.io/x/ref/runtime/factories/roaming" |
| ) |
| |
| const ( |
| testpkg = "v.io/v23/vom/testdata" |
| typespkg = testpkg + "/types" |
| vomdataCanonical = testpkg + "/" + vomdataConfig |
| vomdataConfig = "vomdata.vdl.config" |
| ) |
| |
| var versions = []vom.Version{vom.Version80, vom.Version81} |
| |
| var cmdGenerate = &cmdline.Command{ |
| Runner: cmdline.RunnerFunc(runGenerate), |
| Name: "vomtestgen", |
| Short: "generates test data for the vom wire protocol implementation", |
| Long: ` |
| Command vomtestgen generates test data for the vom wire protocol implementation. |
| It takes as input a vdl config file, and outputs a vdl package with test cases. |
| Test data is generated using this tool to make it easy to add many test cases, |
| and verify them in implementations in different languages |
| `, |
| ArgsName: "[vomdata]", |
| ArgsLong: ` |
| [vomdata] is the path to the vomdata input file, specified in the vdl config |
| file format. It must be of the form "NAME.vdl.config", and the output vdl |
| file will be generated at "NAME.vdl". |
| |
| The config file should export a const []any that contains all of the values |
| that will be tested. Here's an example: |
| config = []any{ |
| bool(true), uint64(123), string("abc"), |
| } |
| |
| If not specified, we'll try to find the file at its canonical location: |
| ` + vomdataCanonical, |
| } |
| |
| var ( |
| optGenMaxErrors int |
| optGenExts string |
| ) |
| |
| func init() { |
| cmdGenerate.Flags.IntVar(&optGenMaxErrors, "max-errors", -1, "Stop processing after this many errors, or -1 for unlimited.") |
| cmdGenerate.Flags.StringVar(&optGenExts, "exts", ".vdl", "Comma-separated list of valid VDL file name extensions.") |
| } |
| |
| func main() { |
| cmdline.Main(cmdGenerate) |
| } |
| |
| func runGenerate(env *cmdline.Env, args []string) error { |
| debug := new(bytes.Buffer) |
| defer dumpDebug(env.Stderr, debug) |
| compileEnv := compile.NewEnv(optGenMaxErrors) |
| // Get the input datafile path. |
| var path string |
| switch len(args) { |
| case 0: |
| srcDirs := build.SrcDirs(compileEnv.Errors) |
| if err := compileEnv.Errors.ToError(); err != nil { |
| return env.UsageErrorf("%v", err) |
| } |
| path = guessDataFilePath(debug, srcDirs) |
| if path == "" { |
| return env.UsageErrorf("couldn't find vomdata file in src dirs: %v", srcDirs) |
| } |
| case 1: |
| path = args[0] |
| default: |
| return env.UsageErrorf("too many args (expecting exactly 1 vomdata file)") |
| } |
| inName := filepath.Clean(path) |
| if !strings.HasSuffix(inName, ".vdl.config") { |
| return env.UsageErrorf(`vomdata file doesn't end in ".vdl.config": %s`, inName) |
| } |
| config, err := compileConfig(debug, inName, compileEnv) |
| if err != nil { |
| return err |
| } |
| for _, version := range versions { |
| baseName := filepath.Base(inName) |
| outName := fmt.Sprintf("%s/data%x/%s", filepath.Dir(inName), version, baseName[:len(baseName)-len(".config")]) |
| // Remove the generated file, so that it doesn't interfere with compiling the |
| // config. Ignore errors since it might not exist yet. |
| if err := os.Remove(outName); err == nil { |
| fmt.Fprintf(debug, "Removed output file %v\n", outName) |
| } |
| data, err := generate(config, version) |
| if err != nil { |
| return err |
| } |
| if err := writeFile(data, outName); err != nil { |
| return err |
| } |
| debug.Reset() // Don't dump debugging information on success |
| fmt.Fprintf(env.Stdout, "Wrote output file %v\n", outName) |
| } |
| return nil |
| } |
| |
| func dumpDebug(stderr io.Writer, debug *bytes.Buffer) { |
| if d := debug.Bytes(); len(d) > 0 { |
| io.Copy(stderr, debug) |
| } |
| } |
| |
| func guessDataFilePath(debug io.Writer, srcDirs []string) string { |
| // Try to guess the data file path by looking for the canonical vomdata input |
| // file in each of our source directories. |
| for _, dir := range srcDirs { |
| guess := filepath.Join(dir, vomdataCanonical) |
| if stat, err := os.Stat(guess); err == nil && !stat.IsDir() { |
| fmt.Fprintf(debug, "Found vomdata file %s\n", guess) |
| return guess |
| } |
| } |
| return "" |
| } |
| |
| func compileConfig(debug io.Writer, inName string, env *compile.Env) (*vdl.Value, error) { |
| basename := filepath.Base(inName) |
| data, err := os.Open(inName) |
| if err != nil { |
| return nil, fmt.Errorf("couldn't open vomdata file %s: %v", inName, err) |
| } |
| defer func() { data.Close() }() // make defer use the latest value of data |
| if stat, err := data.Stat(); err != nil || stat.IsDir() { |
| return nil, fmt.Errorf("vomdata file %s is a directory", inName) |
| } |
| var opts build.Opts |
| opts.Extensions = strings.Split(optGenExts, ",") |
| // Compile package dependencies in transitive order. |
| deps := build.TransitivePackagesForConfig(basename, data, opts, env.Errors) |
| for _, dep := range deps { |
| if pkg := build.BuildPackage(dep, env); pkg != nil { |
| fmt.Fprintf(debug, "Built package %s\n", pkg.Path) |
| } |
| } |
| // Try to seek back to the beginning of the data file, or if that fails try to |
| // open the data file again. |
| if off, err := data.Seek(0, 0); off != 0 || err != nil { |
| if err := data.Close(); err != nil { |
| return nil, fmt.Errorf("couldn't close vomdata file %s: %v", inName, err) |
| } |
| data, err = os.Open(inName) |
| if err != nil { |
| return nil, fmt.Errorf("couldn't re-open vomdata file %s: %v", inName, err) |
| } |
| } |
| // Compile config into our test values. |
| config := build.BuildConfig(basename, data, nil, nil, env) |
| if err := env.Errors.ToError(); err != nil { |
| return nil, err |
| } |
| fmt.Fprintf(debug, "Compiled vomdata file %s\n", inName) |
| return config, err |
| } |
| |
| func generate(config *vdl.Value, version vom.Version) ([]byte, error) { |
| // This config needs to have a specific struct format. See @testdata/vomtype.vdl. |
| // TODO(alexfandrianto): Instead of this, we should have separate generator |
| // functions that switch off of the vomdata config filename. That way, we can |
| // have 1 config each for encode/decode, compatibility, and convertibility. |
| buf := new(bytes.Buffer) |
| fmt.Fprintf(buf, `// Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // This file was auto-generated via "vomtest generate". |
| // DO NOT UPDATE MANUALLY; read the comments in `+vomdataConfig+`. |
| |
| package data%x |
| `, version) |
| imports := codegen.ImportsForValue(config, testpkg) |
| if len(imports) > 0 { |
| fmt.Fprintf(buf, "\n%s\n", vdlgen.Imports(imports)) |
| } |
| typesPkgName := imports.LookupLocal(typespkg) |
| fmt.Fprintf(buf, ` |
| |
| // Tests contains the testcases to use to test vom encoding and decoding. |
| const Tests = []%s.TestCase {`, typesPkgName) |
| // The vom encode-decode test cases need to be of type []any. |
| allEncodeDecodeTests := config.StructField(0) |
| expectedType := vdl.MapType(vdl.ByteType, vdl.ListType(vdl.AnyType)) |
| if got, want := allEncodeDecodeTests.Type(), expectedType; got != want { |
| return nil, fmt.Errorf("got encodeDecodeTests type %v, want %v", got, want) |
| } |
| |
| for _, minVersion := range allEncodeDecodeTests.Keys() { |
| if version < vom.Version(minVersion.Uint()) { |
| continue |
| } |
| encodeDecodeTests := allEncodeDecodeTests.MapIndex(minVersion) |
| for ix := 0; ix < encodeDecodeTests.Len(); ix++ { |
| value := encodeDecodeTests.Index(ix) |
| if !value.IsNil() { |
| // The encodeDecodeTests has type []any, and there's no need for our values to |
| // include the "any" type explicitly, so we descend into the elem value. |
| value = value.Elem() |
| } |
| valstr := vdlgen.TypedConst(value, testpkg, imports) |
| hexversion, hextype, hexvalue, vomdump, err := toVomHex(version, value) |
| if err != nil { |
| return nil, err |
| } |
| fmt.Fprintf(buf, ` |
| %[7]s |
| { |
| %#[1]q, |
| %[1]s, |
| %[2]q, |
| %[3]q, |
| %[4]q, %[5]q, %[6]q, |
| },`, valstr, value.Type().String(), hexversion+hextype+hexvalue, hexversion, hextype, hexvalue, vomdump) |
| } |
| } |
| fmt.Fprintf(buf, ` |
| } |
| `) |
| |
| // The vom compatibility tests need to be of type map[string][]typeobject |
| // Each of the []typeobject are a slice of inter-compatible typeobjects. |
| // However, the typeobjects are not compatible with any other []typeobject. |
| // Note: any and optional should be tested separately. |
| compatTests := config.StructField(1) |
| if got, want := compatTests.Type(), vdl.MapType(vdl.StringType, vdl.ListType(vdl.TypeObjectType)); got != want { |
| return nil, fmt.Errorf("got compatTests type %v, want %v", got, want) |
| } |
| fmt.Fprintf(buf, ` |
| // CompatTests contains the testcases to use to test vom type compatibility. |
| // CompatTests maps TestName (string) to CompatibleTypeSet ([]typeobject) |
| // Each CompatibleTypeSet contains types compatible with each other. However, |
| // types from different CompatibleTypeSets are incompatible. |
| const CompatTests = map[string][]typeobject{`) |
| |
| for _, testName := range vdl.SortValuesAsString(compatTests.Keys()) { |
| compatibleTypeSet := compatTests.MapIndex(testName) |
| valstr := vdlgen.TypedConst(compatibleTypeSet, testpkg, imports) |
| fmt.Fprintf(buf, ` |
| %[1]q: %[2]s,`, testName.RawString(), valstr) |
| } |
| fmt.Fprintf(buf, ` |
| } |
| `) |
| |
| // The vom conversion tests need to be a map[string][]ConvertGroup |
| // See vom/testdata/vomtype.vdl |
| convertTests := config.StructField(2) |
| fmt.Fprintf(buf, ` |
| // ConvertTests contains the testcases to check vom value convertibility. |
| // ConvertTests maps TestName (string) to ConvertGroups ([]ConvertGroup) |
| // Each ConvertGroup is a struct with 'Name', 'PrimaryType', and 'Values'. |
| // The values within a ConvertGroup can convert between themselves w/o error. |
| // However, values in higher-indexed ConvertGroups will error when converting up |
| // to the primary type of the lower-indexed ConvertGroups. |
| const ConvertTests = map[string][]%s.ConvertGroup{`, typesPkgName) |
| for _, testName := range vdl.SortValuesAsString(convertTests.Keys()) { |
| fmt.Fprintf(buf, ` |
| %[1]q: {`, testName.RawString()) |
| |
| convertTest := convertTests.MapIndex(testName) |
| for ix := 0; ix < convertTest.Len(); ix++ { |
| convertGroup := convertTest.Index(ix) |
| |
| fmt.Fprintf(buf, ` |
| { |
| %[1]q, |
| %[2]s, |
| { `, convertGroup.StructField(0).RawString(), vdlgen.TypedConst(convertGroup.StructField(1), testpkg, imports)) |
| |
| values := convertGroup.StructField(2) |
| for iy := 0; iy < values.Len(); iy++ { |
| value := values.Index(iy) |
| if !value.IsNil() { |
| // The value is any, and there's no need for our values to include |
| // the "any" type explicitly, so we descend into the elem value. |
| value = value.Elem() |
| } |
| valstr := vdlgen.TypedConst(value, testpkg, imports) |
| fmt.Fprintf(buf, `%[1]s, `, valstr) |
| } |
| |
| fmt.Fprintf(buf, `}, |
| },`) |
| } |
| |
| fmt.Fprintf(buf, ` |
| },`) |
| } |
| fmt.Fprintf(buf, ` |
| } |
| `) |
| |
| return buf.Bytes(), nil |
| } |
| |
| func toVomHex(version vom.Version, value *vdl.Value) (string, string, string, string, error) { |
| var buf, typebuf bytes.Buffer |
| encoder := vom.NewVersionedEncoderWithTypeEncoder(version, &buf, vom.NewVersionedTypeEncoder(version, &typebuf)) |
| if err := encoder.Encode(value); err != nil { |
| return "", "", "", "", fmt.Errorf("vom.Encode(%v) failed: %v", value, err) |
| } |
| versionByte, _ := buf.ReadByte() // Read the version byte. |
| if typebuf.Len() > 0 { |
| typebuf.ReadByte() // Remove the version byte. |
| } |
| vombytes := append(append([]byte{versionByte}, typebuf.Bytes()...), buf.Bytes()...) |
| const pre = "\t// " |
| var vomdump string |
| if version == 0x80 { |
| vomdump = pre + strings.Replace(vom.Dump(vombytes), "\n", "\n"+pre, -1) |
| } |
| if strings.HasSuffix(vomdump, "\n"+pre) { |
| vomdump = vomdump[:len(vomdump)-len("\n"+pre)] |
| } |
| // TODO(toddw): Add hex pattern bracketing for map and set. |
| return fmt.Sprintf("%x", versionByte), fmt.Sprintf("%x", typebuf.Bytes()), fmt.Sprintf("%x", buf.Bytes()), vomdump, nil |
| } |
| |
| func writeFile(data []byte, outName string) error { |
| // Create containing directory and write the file. |
| dirName := filepath.Dir(outName) |
| if err := os.MkdirAll(dirName, os.FileMode(0777)); err != nil { |
| return fmt.Errorf("couldn't create directory %s: %v", dirName, err) |
| } |
| if err := ioutil.WriteFile(outName, data, os.FileMode(0666)); err != nil { |
| return fmt.Errorf("couldn't write file %s: %v", outName, err) |
| } |
| return nil |
| } |