func validateMain(ctx context.Context, cancel context.CancelFunc, log func(string), interrupt chan os.Signal) int { go func() { <-interrupt cancel() }() env := envctx.FromContext(ctx) changes, err := comparePackageHash(ctx, "kego.io/system") if !changes && err == nil { changes, err = comparePackageHash(ctx, env.Path) } if err != nil { log(err.Error()) return 1 // Exit status 1: generic error } if changes { return 3 // Exit status 3: hash changed error } errors, err := validate.ValidatePackage(ctx) if err != nil { log(err.Error()) return 1 // Exit status 1: generic error } if len(errors) > 0 { for _, e := range errors { log(fmt.Sprintf("%s: %s", e.Source.Path(), e.Description)) } return 4 // Exit status 4: validation error } return 0 // Exit status 0: success }
func main() { // This turns off focus reporting mode that will print “^[[O” and “^[[I” // when the terminal window gets / loses focus // http://superuser.com/questions/931873/o-and-i-appearing-on-iterm2-when-focus-lost fmt.Print("\033[?1004l") ctx, cancel, err := process.Initialise(context.Background(), nil) if err != nil { fmt.Println(err) os.Exit(1) } env := envctx.FromContext(ctx) cmd := cmdctx.FromContext(ctx) c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) signal.Notify(c, syscall.SIGTERM) go func() { <-c cancel() }() if err := process.GenerateAll(ctx, env.Path, map[string]bool{}); err != nil { fmt.Println(err.Error()) wgctx.WaitAndExit(ctx, 1) } if cmd.Validate { err := process.RunValidateCommand(ctx) if err != nil { if !cmd.Log { // in log mode, we have already written the output of the exec'ed ke command, // so we don't need to duplicate the error message. if v, ok := kerr.Source(err).(validate.ValidationCommandError); ok { fmt.Println(v.Description) wgctx.WaitAndExit(ctx, 4) // Exit code 4: validation error } fmt.Println(err.Error()) } wgctx.WaitAndExit(ctx, 1) } } if cmd.Edit { if err = server.Start(ctx, cancel); err != nil { fmt.Println(err.Error()) wgctx.WaitAndExit(ctx, 1) } } wgctx.WaitAndExit(ctx, 0) }
func GetReferencePartsFromTypeString(ctx context.Context, typeString string) (path string, name string, err error) { env := envctx.FromContext(ctx) if strings.Contains(typeString, "/") { // If the type name contains a slash, I'm assuming it's a fully // qualified type name of the form "kego.io/system:type". // TODO: Improve this with a regex? parts := strings.Split(typeString, ":") // We hard-code system and json to prevent them having to always be // specified in the aliases if parts[0] == "kego.io/system" { return "kego.io/system", parts[1], nil } else if parts[0] == "kego.io/json" { return "kego.io/json", parts[1], nil } _, found := findKey(env.Aliases, parts[0]) if !found && parts[0] != env.Path { return "", "", UnknownPackageError{ Struct: kerr.New("KJSOXDESFD", "Unknown package %s", parts[0]), UnknownPackage: parts[0], } } return parts[0], parts[1], nil } else if strings.Contains(typeString, ":") { // If the type name contains a colon, I'm assuming it's an abreviated // qualified type name of the form "system:type". We should look the // package name up in the aliases map. // TODO: Improve this with a regex? parts := strings.Split(typeString, ":") // We hard-code system and json to prevent them having to always be // specified in the aliases if parts[0] == "system" { return "kego.io/system", parts[1], nil } else if parts[0] == "json" { return "kego.io/json", parts[1], nil } packagePath, ok := env.Aliases[parts[0]] if !ok { return "", "", UnknownPackageError{ Struct: kerr.New("DKKFLKDKYI", "Unknown package %s", parts[0]), UnknownPackage: parts[0], } } return packagePath, parts[1], nil } else { return env.Path, typeString, nil } }
func TestInitialise(t *testing.T) { cb := tests.New().TempGopath(true) defer cb.Cleanup() pathA, dirA := cb.TempPackage("a", map[string]string{ "a.json": `{"type": "system:type", "id": "a"}`, "a.go": "package a", }) pathB, dirB := cb.TempPackage("b", map[string]string{ "b.json": `{"type": "system:type", "id": "b"}`, "b.go": "package b", }) cb.OsWd(dirA) ctx, _, err := Initialise(cb.Ctx(), nil) require.NoError(t, err) env := envctx.FromContext(ctx) assert.Equal(t, dirA, env.Dir) assert.Equal(t, pathA, env.Path) cb.OsWd("/") ctx, _, err = Initialise(ctx, &Options{ Path: pathB, }) env = envctx.FromContext(ctx) require.NoError(t, err) assert.Equal(t, dirB, env.Dir) assert.Equal(t, pathB, env.Path) _, _, err = Initialise(ctx, &Options{ Path: "", }) assert.IsError(t, err, "ADNJKTLAWY") assert.HasErrorExternal(t, err, "CXOETFPTGM") }
func testUnpack(t *testing.T, path string) { ctx, _, err := process.Initialise(context.Background(), &process.Options{ Path: path, }) require.NoError(t, err) env := envctx.FromContext(ctx) files := scanner.ScanDirToFiles(ctx, env.Dir, env.Recursive) bytes := scanner.ScanFilesToBytes(ctx, files) for b := range bytes { _, err := node.Unmarshal(ctx, b.Bytes) require.NoError(t, err, b.File) } }
func RunValidateCommand(ctx context.Context) (err error) { env := envctx.FromContext(ctx) validateCommandPath := filepath.Join(env.Dir, validateCommand) build := false repeat := true if _, err := os.Stat(validateCommandPath); os.IsNotExist(err) { build = true repeat = false } if err = runValidateCommand(ctx, build, repeat); err != nil { return kerr.Wrap("KFNIOHWCBT", err) } return nil }
func serve(ctx context.Context) error { wgctx.Add(ctx, "serve") defer wgctx.Done(ctx, "serve") env := envctx.FromContext(ctx) cmd := cmdctx.FromContext(ctx) // Starting with port zero chooses a random open port listner, err := net.Listen("tcp", fmt.Sprintf(":%d", cmd.Port)) if err != nil { return kerr.Wrap("QGLXHWPWQW", err) } defer listner.Close() // Here we get the address we're serving on address, ok := listner.Addr().(*net.TCPAddr) if !ok { return kerr.New("CBLPYVGGUR", "Can't find address (l.Addr() is not *net.TCPAddr)") } url := fmt.Sprintf("http://localhost:%d/%s/", address.Port, env.Path) cmd.Printf("Server now running on %s\n", url) // We open the default browser and navigate to the address we're serving // from. This will error if the system doesn't have a browser, so we can // ignore the error. browser.OpenURL(url) withCancel(ctx, func() { err = http.Serve(listner, nil) }) if err != nil { return kerr.Wrap("TUCBTWMRNN", err) } if cmd.Debug { return kerr.New("ATUTBOICGJ", "Connection closed") } return nil }
// buildValidateCommand creates a temporary folder in the package, in which the go source for the // local command is generated. This command is then compiled. func buildValidateCommand(ctx context.Context) error { wgctx.Add(ctx, "buildValidateCommand") defer wgctx.Done(ctx, "buildValidateCommand") env := envctx.FromContext(ctx) cmd := cmdctx.FromContext(ctx) validateCommandPath := filepath.Join(env.Dir, validateCommand) source, err := generate.ValidateCommand(ctx) if err != nil { return kerr.Wrap("SPRFABSRWK", err) } outputDir, err := ioutil.TempDir(env.Dir, "temporary") if err != nil { return kerr.Wrap("HWOPVXYMCT", err) } defer os.RemoveAll(outputDir) outputName := "generated_cmd.go" outputPath := filepath.Join(outputDir, outputName) if err = save(outputDir, source, outputName, false); err != nil { return kerr.Wrap("FRLCYFOWCJ", err) } cmd.Print("Building validate command... ") combined, stdout, stderr := logger.Logger(cmd.Log) exe := exec.Command("go", "build", "-o", validateCommandPath, outputPath) exe.Stdout = stdout exe.Stderr = stderr if err := exe.Run(); err != nil { return kerr.Wrap("OEPAEEYKIS", err) } cmd.Println("OK.") cmd.Print(combined.String()) return nil }
func ValidatePackage(ctx context.Context) (errors []ValidationError, err error) { env := envctx.FromContext(ctx) files := scanner.ScanDirToFiles(ctx, env.Dir, env.Recursive) bytes := scanner.ScanFilesToBytes(ctx, files) for c := range bytes { if c.Err != nil { return nil, kerr.Wrap("IHSVWAUAYW", c.Err) } ve, err := validateBytes(ctx, c.Bytes) if err != nil { return nil, kerr.Wrap("KWLWXKWHLF", err) } if len(ve) > 0 { errors = append(errors, ve...) } } return }
func ValidateCommand(ctx context.Context) (source []byte, err error) { env := envctx.FromContext(ctx) g := builder.WithName(env.Path, "main") g.Imports.Add("kego.io/process/validate/command") g.Imports.Anonymous("kego.io/system") g.Imports.Anonymous(env.Path) for _, p := range env.Aliases { g.Imports.Anonymous(p) } g.Print(` func main() { command.ValidateMain(` + strconv.Quote(env.Path) + `) }`) b, err := g.Build() if err != nil { return nil, kerr.Wrap("IIIRBBXASR", err) } return b, nil }
func runKe(cb *tests.ContextBuilder, name string, files map[string]string) (string, error) { tests := false for name, _ := range files { if strings.HasSuffix(name, "_test.go") { // if we add a xxx_test.go file we should also run "go test" tests = true } } path, _ := cb.RealGopath().TempPackage(name, files) ctx, _, err := process.Initialise(context.Background(), &process.Options{ Path: path, }) if err != nil { return "", err } env := envctx.FromContext(ctx) if err := process.Generate(ctx, env); err != nil { return "", err } if err := process.RunValidateCommand(ctx); err != nil { return "", err } if tests { if out, err := exec.Command("go", "test", path).CombinedOutput(); err != nil { return "", fmt.Errorf("%s", string(out)) } } return env.Path, nil }
func (c *ContextBuilder) Env() *envctx.Env { return envctx.FromContext(c.ctx) }
func TestRun(t *testing.T) { cb := tests.New().RealGopath() defer cb.Cleanup() path, dir := cb.TempPackage("d", map[string]string{ "a.yaml": ` type: system:type id: a fields: b: type: system:@string max-length: 5`, "d.go": `package d`, }) cb.Path(path).Dir(dir).Jauto().Wg().Sauto(parser.Parse) env := envctx.FromContext(cb.Ctx()) err := Generate(cb.Ctx(), env) require.NoError(t, err) b, err := ioutil.ReadFile(filepath.Join(dir, "generated.go")) require.NoError(t, err) assert.Contains(t, string(b), `pkg.Init(`) assert.Contains(t, string(b), `func() interface{} { return new(A) },`) assert.Contains(t, string(b), `func() interface{} { return new(ARule) },`) assert.Contains(t, string(b), `func() reflect.Type { return reflect.TypeOf((*AInterface)(nil)).Elem() },`) assert.Contains(t, string(b), fmt.Sprintf("%v", env.Hash)) err = RunValidateCommand(cb.Ctx()) require.NoError(t, err) file1, err := os.Stat(filepath.Join(dir, ".localke", "validate")) require.NoError(t, err) time1 := file1.ModTime() err = RunValidateCommand(cb.Ctx()) require.NoError(t, err) cb.TempFile("c.yaml", ` type: a id: c b: foo`) err = RunValidateCommand(cb.Ctx()) require.NoError(t, err) // should not rebuild validate command file2, err := os.Stat(filepath.Join(dir, ".localke", "validate")) require.NoError(t, err) time2 := file2.ModTime() assert.Equal(t, time1, time2) cb.TempFile("e.yaml", ` type: a id: e b: tooolong`) err = RunValidateCommand(cb.Ctx()) assert.IsError(t, err, "KFNIOHWCBT") assert.HasError(t, err, "ETWHPXTUVB") cb.TempFile("f.yaml", ` type: system:type id: f fields: a: type: system:@string`) // This loads the new system.Type into the system cache cb.Sauto(parser.Parse) // This generates a new generated.go err = Generate(cb.Ctx(), env) require.NoError(t, err) // This will re-run the build, but still return the validation error err = RunValidateCommand(cb.Ctx()) assert.IsError(t, err, "KFNIOHWCBT") assert.HasError(t, err, "ETWHPXTUVB") cb.RemoveTempFile("e.yaml") cb.TempFile("h.yaml", ` type: system:type id: h fields: a: type: system:@string`) // This loads the new system.Type into the system cache cb.Sauto(parser.Parse) // This generates a new generated.go err = Generate(cb.Ctx(), env) require.NoError(t, err) // This will re-run the build, but not return the validation error err = RunValidateCommand(cb.Ctx()) require.NoError(t, err) // should rebuild validate command file3, err := os.Stat(filepath.Join(dir, ".localke", "validate")) require.NoError(t, err) time3 := file3.ModTime() assert.NotEqual(t, time1, time3) cb.TempFile("g.yaml", ` type: system:type id: g fields: a: type: system:@string`) // We add a new type, but we haven't generated the struct, so it will fail with hash changed err = runValidateCommand(cb.Ctx(), false, false) assert.IsError(t, err, "DTTHRRJSSF") err = os.Remove(filepath.Join(dir, ".localke", "validate")) require.NoError(t, err) _, err = os.Stat(filepath.Join(dir, ".localke", "validate")) assert.True(t, os.IsNotExist(err)) err = runValidateCommand(cb.Ctx(), false, false) assert.IsError(t, err, "DTTHRRJSSF") _, ok := kerr.Source(err).(*os.PathError) assert.True(t, ok) }
func runValidateCommand(ctx context.Context, build bool, repeat bool) (err error) { wgctx.Add(ctx, "runValidateCommand") defer wgctx.Done(ctx, "runValidateCommand") if build { if err := buildValidateCommand(ctx); err != nil { return kerr.Wrap("FIJHGMEEUM", err) } } env := envctx.FromContext(ctx) cmd := cmdctx.FromContext(ctx) validateCommandPath := filepath.Join(env.Dir, validateCommand) cmd.Print("Running validate command... ") combined, stdout, stderr := logger.Logger(cmd.Log) hashChanged := false exe := exec.Command(validateCommandPath) exe.Stdout = stdout exe.Stderr = stderr if err = exe.Run(); err != nil { exiterr, ok := err.(*exec.ExitError) if !ok { goto Repeat } // The program has exited with an exit code != 0. This works on both Unix and Windows. // Although package syscall is generally platform dependent, WaitStatus is defined for // both Unix and Windows and in both cases has an ExitStatus() method with the same // signature. status, ok := exiterr.Sys().(syscall.WaitStatus) if !ok { // ke: {"block": {"notest": true}} goto Repeat } switch status.ExitStatus() { case 3: // Exit status 3 = hash changed hashChanged = true goto Repeat case 4: // Exit status 4 = validation error return validate.ValidationCommandError{Struct: kerr.New("ETWHPXTUVB", strings.TrimSpace(combined.String()))} default: // ke: {"block": {"notest": true}} goto Repeat } } cmd.Println("OK.") return nil Repeat: if repeat { if hashChanged { cmd.Println("Types have changed since last run. Rebuilding...") } else { // ke: {"block": {"notest": true}} cmd.Println("Command returned an error. Rebuilding...") } if err := runValidateCommand(ctx, true, false); err != nil { return kerr.Wrap("HOHQEISLMI", err) } return nil } return kerr.Wrap("DTTHRRJSSF", err) }