Skip to content

Commit

Permalink
cmd: expose MakeMain() for testing
Browse files Browse the repository at this point in the history
I'd like to add the kargo-demo repository to Unity to test evalv3, but
can't get a handle on the main function to wire up to testscript.

This patch fixes the problem by moving the MakeMain function to a public
package so the kargo-demo go module can import and call it using the go
mod tools technique.
  • Loading branch information
jeffmccune committed Dec 19, 2024
1 parent 54efe3e commit 0e95a28
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 117 deletions.
63 changes: 63 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package cmd

import (
"context"
"fmt"
"log/slog"
"os"
"runtime/pprof"
"runtime/trace"

"github.com/holos-run/holos/internal/cli"
"github.com/holos-run/holos/internal/holos"
)

// MakeMain makes a main function for the cli or tests.
func MakeMain(options ...holos.Option) func() int {
return func() (exitCode int) {
cfg := holos.New(options...)
slog.SetDefault(cfg.Logger())
ctx := context.Background()

if format := os.Getenv("HOLOS_CPU_PROFILE"); format != "" {
f, _ := os.Create(fmt.Sprintf(format, os.Getppid(), os.Getpid()))
err := pprof.StartCPUProfile(f)
defer func() {
pprof.StopCPUProfile()
f.Close()
}()
if err != nil {
return cli.HandleError(ctx, err, cfg)
}
}
defer memProfile(ctx, cfg)

if format := os.Getenv("HOLOS_TRACE"); format != "" {
f, _ := os.Create(fmt.Sprintf(format, os.Getppid(), os.Getpid()))
err := trace.Start(f)
defer func() {
trace.Stop()
f.Close()
}()
if err != nil {
return cli.HandleError(ctx, err, cfg)
}
}

feature := &holos.EnvFlagger{}
if err := cli.New(cfg, feature).ExecuteContext(ctx); err != nil {
return cli.HandleError(ctx, err, cfg)
}
return 0
}
}

func memProfile(ctx context.Context, cfg *holos.Config) {
if format := os.Getenv("HOLOS_MEM_PROFILE"); format != "" {
f, _ := os.Create(fmt.Sprintf(format, os.Getppid(), os.Getpid()))
defer f.Close()
if err := pprof.WriteHeapProfile(f); err != nil {
_ = cli.HandleError(ctx, err, cfg)
}
}
}
4 changes: 2 additions & 2 deletions cmd/holos/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package main
import (
"os"

"github.com/holos-run/holos/internal/cli"
"github.com/holos-run/holos/cmd"
)

func main() {
os.Exit(cli.MakeMain()())
os.Exit(cmd.MakeMain()())
}
4 changes: 2 additions & 2 deletions cmd/holos/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import (
"testing"

cue "cuelang.org/go/cmd/cue/cmd"
"github.com/holos-run/holos/internal/cli"
"github.com/holos-run/holos/cmd"
"github.com/rogpeppe/go-internal/testscript"
)

func TestMain(m *testing.M) {
os.Exit(testscript.RunMain(m, map[string]func() int{
"holos": cli.MakeMain(),
"holos": cmd.MakeMain(),
"cue": cue.Main,
}))
}
Expand Down
109 changes: 0 additions & 109 deletions internal/cli/main.go
Original file line number Diff line number Diff line change
@@ -1,110 +1 @@
package cli

import (
"context"
"fmt"
"log/slog"
"os"
"runtime/pprof"
"runtime/trace"

"connectrpc.com/connect"
cue "cuelang.org/go/cue/errors"
"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/holos"
"google.golang.org/genproto/googleapis/rpc/errdetails"
)

func memProfile(ctx context.Context, cfg *holos.Config) {
if format := os.Getenv("HOLOS_MEM_PROFILE"); format != "" {
f, _ := os.Create(fmt.Sprintf(format, os.Getppid(), os.Getpid()))
defer f.Close()
if err := pprof.WriteHeapProfile(f); err != nil {
_ = HandleError(ctx, err, cfg)
}
}
}

// MakeMain makes a main function for the cli or tests.
func MakeMain(options ...holos.Option) func() int {
return func() (exitCode int) {
cfg := holos.New(options...)
slog.SetDefault(cfg.Logger())
ctx := context.Background()

if format := os.Getenv("HOLOS_CPU_PROFILE"); format != "" {
f, _ := os.Create(fmt.Sprintf(format, os.Getppid(), os.Getpid()))
err := pprof.StartCPUProfile(f)
defer func() {
pprof.StopCPUProfile()
f.Close()
}()
if err != nil {
return HandleError(ctx, err, cfg)
}
}
defer memProfile(ctx, cfg)

if format := os.Getenv("HOLOS_TRACE"); format != "" {
f, _ := os.Create(fmt.Sprintf(format, os.Getppid(), os.Getpid()))
err := trace.Start(f)
defer func() {
trace.Stop()
f.Close()
}()
if err != nil {
return HandleError(ctx, err, cfg)
}
}

feature := &holos.EnvFlagger{}
if err := New(cfg, feature).ExecuteContext(ctx); err != nil {
return HandleError(ctx, err, cfg)
}
return 0
}
}

// HandleError is the top level error handler that unwraps and logs errors.
func HandleError(ctx context.Context, err error, hc *holos.Config) (exitCode int) {
// Connect errors have codes, log them.
log := hc.NewTopLevelLogger().With("code", connect.CodeOf(err))
var cueErr cue.Error
var errAt *errors.ErrorAt

if errors.As(err, &errAt) {
loc := errAt.Source.Loc()
err2 := errAt.Unwrap()
log.ErrorContext(ctx, fmt.Sprintf("could not run: %s at %s", err2, loc), "err", err2, "loc", loc)
} else {
log.ErrorContext(ctx, fmt.Sprintf("could not run: %s", err), "err", err)
}

// cue errors are bundled up as a list and refer to multiple files / lines.
if errors.As(err, &cueErr) {
msg := cue.Details(cueErr, nil)
if _, err := fmt.Fprint(hc.Stderr(), msg); err != nil {
log.ErrorContext(ctx, "could not write CUE error details: "+err.Error(), "err", err)
}
}
// connect errors have details and codes.
// Refer to https://connectrpc.com/docs/go/errors
if connectErr := new(connect.Error); errors.As(err, &connectErr) {
for _, detail := range connectErr.Details() {
msg, valueErr := detail.Value()
if valueErr != nil {
log.WarnContext(ctx, "could not decode error detail", "err", err, "type", detail.Type(), "note", "this usually means we don't have the schema for the protobuf message type")
continue
}
if info, ok := msg.(*errdetails.ErrorInfo); ok {
logDetail := log.With("reason", info.GetReason(), "domain", info.GetDomain())
for k, v := range info.GetMetadata() {
logDetail = logDetail.With(k, v)
}
logDetail.ErrorContext(ctx, info.String())
}
}
}

return 1
}
55 changes: 52 additions & 3 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package cli

import (
"context"
_ "embed"
"fmt"
"log/slog"

"connectrpc.com/connect"
"github.com/spf13/cobra"
"google.golang.org/genproto/googleapis/rpc/errdetails"

"github.com/holos-run/holos/version"

"github.com/holos-run/holos/internal/errors"
"github.com/holos-run/holos/internal/holos"
"github.com/holos-run/holos/internal/logger"
"github.com/holos-run/holos/internal/server"
Expand All @@ -28,7 +32,8 @@ import (
"github.com/holos-run/holos/internal/cli/token"
"github.com/holos-run/holos/internal/cli/txtar"

cue "cuelang.org/go/cmd/cue/cmd"
cueCmd "cuelang.org/go/cmd/cue/cmd"
cue_errors "cuelang.org/go/cue/errors"
)

//go:embed help.txt
Expand Down Expand Up @@ -119,7 +124,7 @@ func newOrgCmd(feature holos.Flagger) (cmd *cobra.Command) {

func newCueCmd() (cmd *cobra.Command) {
// Get a handle on the cue root command fields.
root, _ := cue.New([]string{})
root, _ := cueCmd.New([]string{})
// Copy the fields to our embedded command.
cmd = command.New("cue")
cmd.Short = root.Short
Expand All @@ -130,8 +135,52 @@ func newCueCmd() (cmd *cobra.Command) {

// We do it this way so we handle errors correctly.
cmd.RunE = func(cmd *cobra.Command, args []string) error {
cueRootCommand, _ := cue.New(args)
cueRootCommand, _ := cueCmd.New(args)
return cueRootCommand.Run(cmd.Root().Context())
}
return cmd
}

// HandleError is the top level error handler that unwraps and logs errors.
func HandleError(ctx context.Context, err error, hc *holos.Config) (exitCode int) {
// Connect errors have codes, log them.
log := hc.NewTopLevelLogger().With("code", connect.CodeOf(err))
var cueErr cue_errors.Error
var errAt *errors.ErrorAt

if errors.As(err, &errAt) {
loc := errAt.Source.Loc()
err2 := errAt.Unwrap()
log.ErrorContext(ctx, fmt.Sprintf("could not run: %s at %s", err2, loc), "err", err2, "loc", loc)
} else {
log.ErrorContext(ctx, fmt.Sprintf("could not run: %s", err), "err", err)
}

// cue errors are bundled up as a list and refer to multiple files / lines.
if errors.As(err, &cueErr) {
msg := cue_errors.Details(cueErr, nil)
if _, err := fmt.Fprint(hc.Stderr(), msg); err != nil {
log.ErrorContext(ctx, "could not write CUE error details: "+err.Error(), "err", err)
}
}
// connect errors have details and codes.
// Refer to https://connectrpc.com/docs/go/errors
if connectErr := new(connect.Error); errors.As(err, &connectErr) {
for _, detail := range connectErr.Details() {
msg, valueErr := detail.Value()
if valueErr != nil {
log.WarnContext(ctx, "could not decode error detail", "err", err, "type", detail.Type(), "note", "this usually means we don't have the schema for the protobuf message type")
continue
}
if info, ok := msg.(*errdetails.ErrorInfo); ok {
logDetail := log.With("reason", info.GetReason(), "domain", info.GetDomain())
for k, v := range info.GetMetadata() {
logDetail = logDetail.With(k, v)
}
logDetail.ErrorContext(ctx, info.String())
}
}
}

return 1
}
2 changes: 1 addition & 1 deletion version/embedded/patch
Original file line number Diff line number Diff line change
@@ -1 +1 @@
7
8

0 comments on commit 0e95a28

Please sign in to comment.