From e639c7b0fd9da23b3bf629f90bf0662f0b04028c Mon Sep 17 00:00:00 2001 From: Jon Webb Date: Mon, 7 Oct 2024 01:00:00 -0400 Subject: [PATCH] implement testgen in C --- .gitignore | 2 +- Makefile | 4 +- cmd/testgen/main.go | 37 ---- getopt.go | 6 + getopt_fixtures_test.go | 211 +++++++++--------- getopt_test.go | 18 +- internal/testgen/cgetopt.go | 208 ------------------ internal/testgen/fixture.go | 182 --------------- testgen/case.c | 114 ---------- testgen/case.h | 18 -- testgen/config.c | 79 ------- testgen/config.h | 12 - testgen/generate.h | 4 - testgen/iterate.c | 117 ---------- testgen/iterate.h | 26 --- testgen/main.c | 428 ++++++++++++++++++++++++++++++++++-- 16 files changed, 550 insertions(+), 916 deletions(-) delete mode 100644 cmd/testgen/main.go delete mode 100644 internal/testgen/cgetopt.go delete mode 100644 internal/testgen/fixture.go delete mode 100644 testgen/case.c delete mode 100644 testgen/case.h delete mode 100644 testgen/config.c delete mode 100644 testgen/config.h delete mode 100644 testgen/generate.h delete mode 100644 testgen/iterate.c delete mode 100644 testgen/iterate.h diff --git a/.gitignore b/.gitignore index 936d42d..9fa6107 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,7 @@ go.work .vscode/* !.vscode/extensions.json -.vgcore.* +vgcore.* *.log *.local *.o diff --git a/Makefile b/Makefile index 401e7be..cfe29a3 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ TMPDIR := tmp OBJDIR := obj CC := gcc -CFLAGS_BASE := -std=c23 -Werror -Wall -Wextra -Wpedantic -Wno-unused-parameter -Wshadow -Wwrite-strings -Wstrict-prototypes -Wold-style-definition -Wredundant-decls -Wnested-externs -Wmissing-include-dirs -Wjump-misses-init -Wlogical-op +CFLAGS_BASE := -std=c23 -Wall -Wextra -Wpedantic -Wno-unused-parameter -Wshadow -Wwrite-strings -Wstrict-prototypes -Wold-style-definition -Wredundant-decls -Wnested-externs -Wmissing-include-dirs -Wjump-misses-init -Wlogical-op CFLAGS := $(CFLAGS_BASE) -O2 $(TESTGEN_INCL) CFLAGS_DEBUG := $(CFLAGS_BASE) -g -O0 $(TESTGEN_INCL) @@ -105,7 +105,7 @@ $(OBJDIR)/%_debug.o: $(TESTGEN_SRCDIR)/%.c | $(OBJDIR) ## testgen-check: run testgen with valgrind .PHONY: testgen-check testgen-check: $(TESTGEN_DEBUG_BIN) - valgrind --leak-check=full $< -o $(TESTGEN_OUTPUT) $(TESTGEN_INPUT) + valgrind --leak-check=full --track-origins=yes $< -o $(TESTGEN_OUTPUT) $(TESTGEN_INPUT) ## testgen-debug: run testgen with gdb .PHONY: testgen-debug diff --git a/cmd/testgen/main.go b/cmd/testgen/main.go deleted file mode 100644 index af9bcd0..0000000 --- a/cmd/testgen/main.go +++ /dev/null @@ -1,37 +0,0 @@ -//go:build linux - -package main - -import ( - "fmt" - "os" - - "github.com/jon-codes/getopt/internal/testgen" -) - -const ( - inpath = "testdata/cases.json" - outpath = "testdata/fixtures.json" -) - -func main() { - infile, err := os.Open(inpath) - if err != nil { - fmt.Fprintf(os.Stderr, "error opening infile: %v\n", err) - os.Exit(1) - } - defer infile.Close() - - outfile, err := os.Create(outpath) - if err != nil { - fmt.Fprintf(os.Stderr, "error opening outfile: %v\n", err) - os.Exit(1) - } - defer outfile.Close() - - err = testgen.ProcessCases(infile, outfile) - if err != nil { - fmt.Fprintf(os.Stderr, "error processing test cases: %v\n", err) - os.Exit(1) - } -} diff --git a/getopt.go b/getopt.go index 9a8463e..7dd78dc 100644 --- a/getopt.go +++ b/getopt.go @@ -243,6 +243,12 @@ func (s *State) readOpt(p Params) (res Result, err error) { s.OptIndex++ } } else { + s.argIndex++ + if arg[s.argIndex:] == "" { + s.OptIndex++ + s.argIndex = 0 + } else { + } err = ErrUnknownOpt } } diff --git a/getopt_fixtures_test.go b/getopt_fixtures_test.go index 1369c6f..bba6c27 100644 --- a/getopt_fixtures_test.go +++ b/getopt_fixtures_test.go @@ -6,14 +6,118 @@ import ( "fmt" "os" "slices" + "strings" "testing" - "unicode/utf8" - - "github.com/jon-codes/getopt/internal/testgen" ) const fixturePath = "testdata/fixtures.json" +type fixtureRecordIter struct { + Opt int `json:"opt"` + OptInd int `json:"optind"` + OptOpt int `json:"optopt"` + OptArg string `json:"optarg"` + LongIndex int `json:"longindex"` +} + +type fixtureIter struct { + Char rune + Name string + OptArg string + Err error +} + +func (fri *fixtureRecordIter) toFixtureIter(fixture *fixture) fixtureIter { + fi := fixtureIter{ + OptArg: fri.OptArg, + } + + switch fri.Opt { + case ':': + fi.Err = ErrUnknownOpt + case '?': + fi.Err = ErrUnknownOpt + if fri.OptOpt > 0 { + fi.Char = rune(fri.OptOpt) + } else if fixture.Function != FuncGetOpt { + name := strings.TrimLeft(fixture.Args[fri.OptInd-1], "-") + name = strings.SplitN(name, "=", 2)[0] + fi.Name = name + } + case -1: // done + fi.Err = ErrDone + case -2: + fi.Name = fixture.LongOpts[fri.LongIndex].Name + default: + fi.Char = rune(fri.Opt) + } + + return fi +} + +type fixtureRecord struct { + Label string `json:"label"` + Func string `json:"func"` + Mode string `json:"mode"` + Args []string `json:"args"` + Opts string `json:"opts"` + Lopts string `json:"lopts"` + WantArgs []string `json:"want_args"` + WantOptInd int `json:"want_optind"` + WantResults []fixtureRecordIter `json:"want_results"` +} + +type fixture struct { + Label string + Function GetOptFunc + Mode GetOptMode + Opts []Opt + LongOpts []LongOpt + Args []string + WantArgs []string + WantResults []fixtureIter + WantOptInd int +} + +func (fr *fixtureRecord) toFixture() (fixture, error) { + f := fixture{ + Label: fr.Label, + Args: fr.Args, + Opts: OptStr(fr.Opts), + LongOpts: LongOptStr(fr.Lopts), + WantArgs: fr.WantArgs, + WantOptInd: fr.WantOptInd, + } + + switch fr.Func { + case "getopt": + f.Function = FuncGetOpt + case "getopt_long": + f.Function = FuncGetOptLong + case "getopt_long_only": + f.Function = FuncGetOptLongOnly + default: + return f, fmt.Errorf("unknown function type %q", fr.Func) + } + + switch fr.Mode { + case "gnu": + f.Mode = ModeGNU + case "posix": + f.Mode = ModePosix + case "inorder": + f.Mode = ModeInOrder + default: + return f, fmt.Errorf("unknown mode type %q", fr.Mode) + } + + for _, fri := range fr.WantResults { + f.WantResults = append(f.WantResults, fri.toFixtureIter(&f)) + } + + return f, nil +} + func TestGetOpt_Fixtures(t *testing.T) { fixtureFile, err := os.Open(fixturePath) if err != nil { @@ -31,15 +135,15 @@ func TestGetOpt_Fixtures(t *testing.T) { // while the array contains values for decoder.More() { - var record testgen.FixtureRecord + var record fixtureRecord if err := decoder.Decode(&record); err != nil { t.Fatalf("error decoding fixture: %v", err) } - fixture, err := buildFixture(record) + fixture, err := record.toFixture() if err != nil { t.Fatalf("error parsing fixture: %v", err) } - testName := fmt.Sprintf("Fixture %q (function %q, mode %q)", record.Label, record.FunctionStr, record.ModeStr) + testName := fmt.Sprintf("Fixture %q (function %q, mode %q)", record.Label, record.Func, record.Mode) t.Run(testName, func(t *testing.T) { assertFixture(t, fixture) }) @@ -89,102 +193,11 @@ func assertFixture(t testing.TB, f fixture) { } } - if s.OptIndex != f.WantOptIndex { - t.Errorf("got OptIndex %d, but wanted %d", s.OptIndex, f.WantOptIndex) + if s.OptIndex != f.WantOptInd { + t.Errorf("got OptIndex %d, but wanted %d", s.OptIndex, f.WantOptInd) } if !slices.Equal(s.Args, f.WantArgs) { t.Errorf("got Args %+q, but wanted %+q", s.Args, f.WantArgs) } } - -type fixtureIter struct { - Char rune - Name string - OptArg string - Err error -} - -type fixture struct { - Label string - Args []string - Opts []Opt - LongOpts []LongOpt - Function GetOptFunc - Mode GetOptMode - WantArgs []string - WantOptIndex int - WantResults []fixtureIter -} - -func buildFixture(fr testgen.FixtureRecord) (f fixture, err error) { - var function GetOptFunc - switch fr.FunctionStr { - case "getopt": - function = FuncGetOpt - case "getopt_long": - function = FuncGetOptLong - case "getopt_long_only": - function = FuncGetOptLongOnly - default: - return f, fmt.Errorf("unknown function type %q", fr.FunctionStr) - } - - var mode GetOptMode - switch fr.ModeStr { - case "gnu": - mode = ModeGNU - case "posix": - mode = ModePosix - case "inorder": - mode = ModeInOrder - default: - return f, fmt.Errorf("unknown mode type %q", fr.ModeStr) - } - - var wantResults []fixtureIter - for _, fi := range fr.WantResults { - var char rune - if fi.CharStr != "" { - char, _ = utf8.DecodeRuneInString(fi.CharStr) - } - - var err error - switch fi.ErrStr { - case "": - err = nil - case "-1": - err = ErrDone - case ":": - err = ErrMissingOptArg - case "?": - _, found := findLongOpt(fi.Name, false, Params{LongOpts: LongOptStr(fr.LongOptStr), Function: function, Mode: mode}) - if fi.Name != "" && found { - err = ErrIllegalOptArg - } else { - err = ErrUnknownOpt - } - default: - return f, fmt.Errorf("unknown error type %q", fi.ErrStr) - } - - wantResults = append(wantResults, fixtureIter{ - Char: char, - Name: fi.Name, - OptArg: fi.OptArg, - Err: err, - }) - } - - f.Label = fr.Label - f.Args = argsStr(fr.ArgsStr) - f.Opts = OptStr(fr.OptStr) - f.LongOpts = LongOptStr(fr.LongOptStr) - f.Function = function - f.Mode = mode - f.WantArgs = argsStr(fr.WantArgsStr) - f.WantOptIndex = fr.WantOptIndex - f.WantResults = wantResults - - return f, nil -} diff --git a/getopt_test.go b/getopt_test.go index b5edb6c..588c568 100644 --- a/getopt_test.go +++ b/getopt_test.go @@ -401,7 +401,7 @@ func TestGetOpt_FuncGetOpt(t *testing.T) { p := Params{Opts: OptStr(`b`), Function: function} wants := []assertion{ - {char: 'a', err: ErrUnknownOpt, args: argsStr(`prgm -a`), optIndex: 1}, + {char: 'a', err: ErrUnknownOpt, args: argsStr(`prgm -a`), optIndex: 2}, } assertSeq(t, s, p, wants) @@ -573,6 +573,12 @@ func TestGetOpt_FuncGetOpt(t *testing.T) { wants := []assertion{ {char: '-', err: ErrUnknownOpt, args: argsStr(`prgm --longa`), optIndex: 1}, + {char: 'l', err: ErrUnknownOpt, args: argsStr(`prgm --longa`), optIndex: 1}, + {char: 'o', err: ErrUnknownOpt, args: argsStr(`prgm --longa`), optIndex: 1}, + {char: 'n', err: ErrUnknownOpt, args: argsStr(`prgm --longa`), optIndex: 1}, + {char: 'g', err: ErrUnknownOpt, args: argsStr(`prgm --longa`), optIndex: 1}, + {char: 'a', err: ErrUnknownOpt, args: argsStr(`prgm --longa`), optIndex: 2}, + {err: ErrDone, args: argsStr(`prgm --longa`), optIndex: 2}, } assertSeq(t, s, p, wants) @@ -584,6 +590,11 @@ func TestGetOpt_FuncGetOpt(t *testing.T) { wants := []assertion{ {char: 'l', err: ErrUnknownOpt, args: argsStr(`prgm -longa`), optIndex: 1}, + {char: 'o', err: ErrUnknownOpt, args: argsStr(`prgm -longa`), optIndex: 1}, + {char: 'n', err: ErrUnknownOpt, args: argsStr(`prgm -longa`), optIndex: 1}, + {char: 'g', err: ErrUnknownOpt, args: argsStr(`prgm -longa`), optIndex: 1}, + {char: 'a', err: ErrUnknownOpt, args: argsStr(`prgm -longa`), optIndex: 2}, + {err: ErrDone, args: argsStr(`prgm -longa`), optIndex: 2}, } assertSeq(t, s, p, wants) @@ -637,6 +648,11 @@ func TestGetOpt_FuncGetOptLong(t *testing.T) { wants := []assertion{ {char: 'l', err: ErrUnknownOpt, args: argsStr(`prgm -longa`), optIndex: 1}, + {char: 'o', err: ErrUnknownOpt, args: argsStr(`prgm -longa`), optIndex: 1}, + {char: 'n', err: ErrUnknownOpt, args: argsStr(`prgm -longa`), optIndex: 1}, + {char: 'g', err: ErrUnknownOpt, args: argsStr(`prgm -longa`), optIndex: 1}, + {char: 'a', err: ErrUnknownOpt, args: argsStr(`prgm -longa`), optIndex: 2}, + {err: ErrDone, args: argsStr(`prgm -longa`), optIndex: 2}, } assertSeq(t, s, p, wants) diff --git a/internal/testgen/cgetopt.go b/internal/testgen/cgetopt.go deleted file mode 100644 index 685b859..0000000 --- a/internal/testgen/cgetopt.go +++ /dev/null @@ -1,208 +0,0 @@ -package testgen - -/* -#include -#include - -char* get_optarg() { - return optarg; -} - -int get_optind() { - return optind; -} - -int get_optopt() { - return optopt; -} - -int reset_getopt() { - optind = 0; -} -*/ -import "C" - -import ( - "fmt" - "strings" - "unsafe" -) - -type cGetOptResult struct { - char string - name string - err string - optind int - optarg string - args []string -} - -func BuildCArgv(args []string) (C.int, []*C.char, func()) { - cArgc := C.int(len(args)) - cArgv := make([]*C.char, cArgc) - for i, arg := range args { - cArgv[i] = C.CString(arg) - } - free := func() { - for i := 0; i < len(args); i++ { - C.free(unsafe.Pointer(cArgv[i])) - } - } - return cArgc, cArgv, free -} - -func buildCOptstring(optstring string, mode string) (*C.char, func()) { - optstring = ":" + optstring // always act like opterr = 0 - - if mode == "posix" { // posixly_correct - optstring = "+" + optstring - } - if mode == "inorder" { // inorder - optstring = "-" + optstring - } - cOptstring := C.CString(optstring) - free := func() { - C.free(unsafe.Pointer(cOptstring)) - } - return cOptstring, free -} - -func buildCLongoptions(longoptstring string, flag *C.int) ([]C.struct_option, func()) { - longoptions := []C.struct_option{} - - opts := strings.Split(longoptstring, ",") - for idx, opt := range opts { - name := strings.TrimSpace(opt) - if name == "" { - continue - } - hasArg := 0 // no_argument - name, found := strings.CutSuffix(opt, "::") - if found { - hasArg = 2 // optional_argument - } else { - name, found = strings.CutSuffix(opt, ":") - if found { - hasArg = 1 // required_argument - } - } - - longoptions = append(longoptions, C.struct_option{ - name: C.CString(name), - has_arg: C.int(hasArg), - flag: flag, - val: C.int(-(idx + 1)), - }) - } - // null terminator - longoptions = append(longoptions, C.struct_option{name: nil, has_arg: 0, flag: nil, val: 0}) - - free := func() { - for _, opt := range longoptions { - if opt.name != nil { - C.free(unsafe.Pointer(opt.name)) - } - } - } - return longoptions, free -} - -func parseRet(ret int, optopt int) (string, string) { - char := "" - err := "" - if ret == -1 { - err = "-1" - } else if ret == ':' || ret == '?' { - err = string(rune(ret)) - if optopt > 0 { - char = string(rune(optopt)) - } - } else if ret != 0 { - char = string(rune(ret)) - } - return char, err -} - -func parseName(cLongoptions []C.struct_option, char string, optopt int, flag *C.int) string { - name := "" - if char != "" && char != ":" && char != "?" { - return name - } - if optopt < 0 { - name = C.GoString(cLongoptions[(-(optopt) - 1)].name) - } - if *flag < 0 { - name = C.GoString(cLongoptions[(-(*flag) - 1)].name) - } - return name -} - -func copyCArgv(cArgc C.int, cArgv []*C.char) []string { - args := make([]string, cArgc) - for i := 0; i < int(cArgc); i++ { - args[i] = C.GoString(cArgv[i]) - } - return args -} - -func cGetOpt(cArgc C.int, cArgv []*C.char, optstring string, longoptstring string, function string, mode string) (cGetOptResult, error) { - cOptstring, freeCOptstring := buildCOptstring(optstring, mode) - defer freeCOptstring() - - flag := (*C.int)(C.malloc(C.sizeof_int)) - *flag = 0 - defer C.free(unsafe.Pointer(flag)) - - cLongoptions, freeCLongoptions := buildCLongoptions(longoptstring, flag) - defer freeCLongoptions() - - var cLongindex C.int - var ret int - - switch function { - case "getopt": - ret = int(C.getopt(cArgc, &cArgv[0], cOptstring)) - case "getopt_long": - ret = int(C.getopt_long(cArgc, &cArgv[0], cOptstring, &cLongoptions[0], &cLongindex)) - case "getopt_long_only": - ret = int(C.getopt_long_only(cArgc, &cArgv[0], cOptstring, &cLongoptions[0], &cLongindex)) - default: - return cGetOptResult{}, fmt.Errorf("unknown function type: %q", function) - } - - optarg := C.GoString(C.get_optarg()) - optind := int(C.get_optind()) - optopt := int(C.get_optopt()) - curr_arg := C.GoString(cArgv[optind-1]) - - char, err := parseRet(ret, optopt) - name := parseName(cLongoptions, char, optopt, flag) - mutArgs := copyCArgv(cArgc, cArgv) - - if err == "-1" { - optarg = "" - } - - if (err == "?" || err == ":") && (char == "" && name == "") { - val := strings.TrimPrefix(curr_arg, "-") - val = strings.TrimPrefix(val, "-") - if len(val) == 1 { - char = val - } else { - name = val - } - } - - return cGetOptResult{ - char: char, - name: name, - err: err, - optind: optind, - optarg: optarg, - args: mutArgs, - }, nil -} - -func cResetGetOpt() { - C.reset_getopt() -} diff --git a/internal/testgen/fixture.go b/internal/testgen/fixture.go deleted file mode 100644 index 6655eee..0000000 --- a/internal/testgen/fixture.go +++ /dev/null @@ -1,182 +0,0 @@ -package testgen - -import ( - "encoding/json" - "fmt" - "io" - "os" - "strings" - "unicode" -) - -type caseRecord struct { - Label string `json:"label"` - ArgsStr string `json:"args"` - OptStr string `json:"opts"` - LongOptStr string `json:"longopts"` -} - -func (c caseRecord) Args() (args []string) { - return argsStr(c.ArgsStr) -} - -var ( - modes = []string{"gnu", "posix", "inorder"} - functions = []string{"getopt", "getopt_long", "getopt_long_only"} -) - -type fixtureIter struct { - CharStr string `json:"char"` - Name string `json:"name"` - OptArg string `json:"opt_arg"` - ErrStr string `json:"err"` -} - -type FixtureRecord struct { - Label string `json:"label"` - ArgsStr string `json:"args"` - OptStr string `json:"opts"` - LongOptStr string `json:"longopts"` - FunctionStr string `json:"function"` - ModeStr string `json:"mode"` - WantArgsStr string `json:"want_args"` - WantOptIndex int `json:"want_optindex"` - WantResults []fixtureIter `json:"want_results"` -} - -func generateCaseFixtures(w io.Writer, c caseRecord, more bool) (err error) { - for i, function := range functions { - for j, mode := range modes { - step := 0 - - f := FixtureRecord{ - Label: c.Label, - ArgsStr: c.ArgsStr, - OptStr: c.OptStr, - LongOptStr: c.LongOptStr, - FunctionStr: function, - ModeStr: mode, - } - cArgc, cArgv, free := BuildCArgv(c.Args()) - - for { - res, err := cGetOpt(cArgc, cArgv, c.OptStr, c.LongOptStr, function, mode) - if err != nil { - return fmt.Errorf("error generating case fixture: %v", err) - } - - f.WantResults = append(f.WantResults, fixtureIter{ - CharStr: res.char, - Name: res.name, - OptArg: res.optarg, - ErrStr: res.err, - }) - - if res.err != "" { - // save the final args & optindex - f.WantArgsStr = strings.Join(res.args, " ") - f.WantOptIndex = res.optind - - free() - cResetGetOpt() - break - } - - step++ - } - - data, err := json.MarshalIndent(f, "\t", "\t") - if err != nil { - return fmt.Errorf("error marshalling case fixture: %v", err) - } - _, err = w.Write(data) - if err != nil { - return fmt.Errorf("error writing to fixture file: %v", err) - } - - if !(i == len(functions)-1 && j == len(modes)-1) { - _, err = w.Write([]byte(",\n\t")) - if err != nil { - return fmt.Errorf("error writing to fixture file: %v", err) - } - } - } - } - - if more { - _, err = w.Write([]byte(",\n\t")) - if err != nil { - return fmt.Errorf("error writing to fixture file: %v", err) - } - } - - return nil -} - -func ProcessCases(in *os.File, out *os.File) error { - decoder := json.NewDecoder(in) - _, err := out.WriteString("[\n\t") - if err != nil { - return fmt.Errorf("error writing to fixture file: %v", err) - } - - // read open bracket - _, err = decoder.Token() - if err != nil { - return fmt.Errorf("error decoding cases: %v", err) - } - - // while the array contains values - for decoder.More() { - var c caseRecord - if err := decoder.Decode(&c); err != nil { - return fmt.Errorf("error decoding cases: %v", err) - } - err = generateCaseFixtures(out, c, decoder.More()) - if err != nil { - return fmt.Errorf("error generating case fixtures: %v", err) - } - } - - // read closing bracket - _, err = decoder.Token() - if err != nil { - return fmt.Errorf("error decoding cases: %v", err) - } - - _, err = out.WriteString("]") - if err != nil { - return fmt.Errorf("error writing to fixture file: %v", err) - } - - return nil -} - -func argsStr(argsStr string) (args []string) { - // TODO: this parsing is extremely basic and should be expanded to include scenarios like https://github.com/google/shlex - - var current strings.Builder - inSingle := false - inDouble := false - - for _, r := range argsStr { - switch { - case r == '"' && !inSingle: - current.WriteRune(r) - case r == '\'' && !inDouble: - current.WriteRune(r) - case unicode.IsSpace(r) && !inSingle && !inDouble: - if current.Len() > 0 { - args = append(args, current.String()) - current.Reset() - } - default: - current.WriteRune(r) - } - } - if current.Len() > 0 { - args = append(args, current.String()) - } - - return args -} diff --git a/testgen/case.c b/testgen/case.c deleted file mode 100644 index 3c09229..0000000 --- a/testgen/case.c +++ /dev/null @@ -1,114 +0,0 @@ -#include "case.h" - -#include -#include -#include -#include - -void case_clear(Case *c) { - if (c != NULL) { - free(c->label); - free(c->opts); - free(c->lopts); - if (c->argv != NULL) { - for (int i = 0; i < c->argc; i++) { - free(c->argv[i]); - } - free(c->argv); - } - - c->label = NULL; - c->opts = NULL; - c->lopts = NULL; - c->argv = NULL; - c->argc = 0; - } -} - -bool read_case(Case *dest, FILE *src) { - size_t flags = JSON_DISABLE_EOF_CHECK; - json_t *json_value; - json_error_t json_err; - - json_value = json_loadf(src, flags, &json_err); - if (!json_value) { - fprintf(stderr, "JSON parsing error: %s\n", json_err.text); - return true; - } - - json_t *args; - const char *label, *opts, *lopts; - int err = json_unpack_ex(json_value, &json_err, 0, "{s:s, s:o, s:s, s:s}", - "label", &label, - "args", &args, - "opts", &opts, - "lopts", &lopts); - - if (err != 0) { - fprintf(stderr, "JSON parsing error: %s %s\n", json_err.text, json_err.source); - json_decref(json_value); - return true; - } - - if (!json_is_array(args)) { - fprintf(stderr, "expected args to be an array\n"); - json_decref(json_value); - return true; - } - - dest->label = strdup(label); - dest->opts = strdup(opts); - dest->lopts = strdup(lopts); - - if (!dest->label || !dest->opts || !dest->lopts) { - fprintf(stderr, "Memory allocation error\n"); - free(dest->label); - free(dest->opts); - free(dest->lopts); - json_decref(json_value); - return true; - } - - dest->argc = json_array_size(args); - dest->argv = malloc(dest->argc * sizeof(char *)); - if (dest->argv == NULL) { - fprintf(stderr, "Memory allocation error for args\n"); - free(dest->label); - free(dest->opts); - free(dest->lopts); - json_decref(json_value); - return true; - } - - for (int i = 0; i < dest->argc; i++) { - json_t *el = json_array_get(args, i); - if (!json_is_string(el)) { - fprintf(stderr, "expected args element to be a string\n"); - for (int j = 0; j < i; j++) { - free(dest->argv[j]); - } - free(dest->argv); - free(dest->label); - free(dest->opts); - free(dest->lopts); - json_decref(json_value); - return true; - } - dest->argv[i] = strdup(json_string_value(el)); - if (dest->argv[i] == NULL) { - fprintf(stderr, "Memory allocation error for arg %d\n", i); - for (int j = 0; j < i; j++) { - free(dest->argv[j]); - } - free(dest->argv); - free(dest->label); - free(dest->opts); - free(dest->lopts); - json_decref(json_value); - return true; - } - } - - json_decref(json_value); - return false; -} diff --git a/testgen/case.h b/testgen/case.h deleted file mode 100644 index 13bacc4..0000000 --- a/testgen/case.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef CASE_H -#define CASE_H - -#include -#include - -typedef struct Case { - char *label; - char *opts; - char *lopts; - char **argv; - int argc; -} Case; - -bool read_case(Case *dest, FILE *src); -void case_clear(Case *c); - -#endif diff --git a/testgen/config.c b/testgen/config.c deleted file mode 100644 index 6d9e309..0000000 --- a/testgen/config.c +++ /dev/null @@ -1,79 +0,0 @@ - -#include "config.h" - -#include -#include -#include -#include -#include - -static void print_usage(const char *name); - -Config *create_config(int argc, char *argv[]) { - Config *cfg = calloc(1, sizeof(Config)); - if (cfg == NULL) { - fprintf(stderr, "error allocating config: %s\n", strerror(errno)); - return NULL; - } - - int opt; - while ((opt = getopt(argc, argv, ":o:")) != -1) { - switch (opt) { - case 'o': - cfg->outpath = strdup(optarg); - if (cfg->outpath == NULL) { - fprintf(stderr, "error allocating config: %s\n", strerror(errno)); - config_destroy(cfg); - return NULL; - } - break; - case '?': - fprintf(stderr, "error: Unknown option \"%c\"\n", optopt); - print_usage(argv[0]); - config_destroy(cfg); - return NULL; - case ':': - fprintf(stderr, "error: Option \"%c\" requires an argument\n", optopt); - print_usage(argv[0]); - config_destroy(cfg); - return NULL; - default: - break; - } - } - - if (cfg->outpath == NULL) { - fprintf(stderr, "error: Option -o is required\n"); - print_usage(argv[0]); - config_destroy(cfg); - return NULL; - } - - if (optind < argc) { - cfg->inpath = strdup(argv[optind]); - if (cfg->inpath == NULL) { - fprintf(stderr, "error allocating config: %s\n", strerror(errno)); - config_destroy(cfg); - return NULL; - } - } else { - fprintf(stderr, "error: missing required infile parameter\n"); - print_usage(argv[optind]); - config_destroy(cfg); - return NULL; - } - - return cfg; -} - -void config_destroy(Config *cfg) { - if (cfg != NULL) { - free((char *)cfg->outpath); - free((char *)cfg->inpath); - free(cfg); - } -} - -static void print_usage(const char *name) { - fprintf(stderr, "usage: %s -o \n", name); -} diff --git a/testgen/config.h b/testgen/config.h deleted file mode 100644 index 9850606..0000000 --- a/testgen/config.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef CONFIG_H -#define CONFIG_H - -typedef struct Config { - const char *inpath; - const char *outpath; -} Config; - -Config *create_config(int argc, char *argv[]); -void config_destroy(Config *cfg); - -#endif diff --git a/testgen/generate.h b/testgen/generate.h deleted file mode 100644 index 31cfb9f..0000000 --- a/testgen/generate.h +++ /dev/null @@ -1,4 +0,0 @@ -#ifndef GENERATE_H -#define GENERATE_H - -#endif diff --git a/testgen/iterate.c b/testgen/iterate.c deleted file mode 100644 index 48ce017..0000000 --- a/testgen/iterate.c +++ /dev/null @@ -1,117 +0,0 @@ -#include "iterate.h" - -#include -#include -#include -#include -#include - -#include "case.h" -#include "config.h" -#include "stdbool.h" - -static bool seek_array_start(FILE *f); -static enum IteratorStatus seek_next_element(FILE *f); -static FILE *prepare_infile(const Config *cfg); - -Iterator *create_iterator(Config *cfg) { - Iterator *iter = calloc(1, sizeof(Iterator)); - if (iter == NULL) { - fprintf(stderr, "error allocating iterator: %s\n", strerror(errno)); - return NULL; - } - - iter->src = prepare_infile(cfg); - if (iter->src == NULL) { - free(iter); - return NULL; - } - - iter->status = ITER_OK; - iter->index = -1; - - return iter; -} - -void iterator_destroy(Iterator *iter) { - if (iter != NULL) { - if (iter->src != NULL) { - fclose(iter->src); - } - case_clear(&iter->current); - free(iter); - } -} - -void iterator_next(Iterator *iter) { - if (iter->status != ITER_OK) { - return; - } - - if (iter->index != -1) { - iter->status = seek_next_element(iter->src); - if (iter->status != ITER_OK) { - return; - } - } - - case_clear(&iter->current); - if (read_case(&iter->current, iter->src)) { - iter->status = ITER_ERROR; - return; - } - - iter->index++; -} - -static bool seek_array_start(FILE *f) { - for (;;) { - int c = fgetc(f); - if (c == EOF || !isspace(c)) { - if (c != '[') { - fprintf(stderr, "error: Input is not a valid json array\n"); - return true; - } - break; - } - } - return false; -} - -static enum IteratorStatus seek_next_element(FILE *f) { - if (f == NULL) { - fprintf(stderr, "error: Invalid file pointer\n"); - return ITER_ERROR; - } - - int c; - while ((c = fgetc(f)) != EOF) { - switch (c) { - case ']': - return ITER_DONE; - case ',': - return ITER_OK; - default: - if (!isspace(c)) { - fprintf(stderr, "error: Input is not a valid json array\n"); - return ITER_ERROR; - } - } - } - fprintf(stderr, "error: Unexpected EOF\n"); - return ITER_ERROR; -} - -static FILE *prepare_infile(const Config *cfg) { - FILE *f = fopen(cfg->inpath, "r"); - if (f == NULL) { - fprintf(stderr, "error opening iterator source %s: %s\n", cfg->inpath, strerror(errno)); - return NULL; - } - - if (seek_array_start(f) != 0) { - return NULL; - } - - return f; -} diff --git a/testgen/iterate.h b/testgen/iterate.h deleted file mode 100644 index 6785205..0000000 --- a/testgen/iterate.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef ITERATE_H -#define ITERATE_H - -#include - -#include "case.h" -#include "config.h" - -typedef enum IteratorStatus { - ITER_OK, - ITER_ERROR, - ITER_DONE, -} IteratorStatus; - -typedef struct Iterator { - FILE *src; - Case current; - IteratorStatus status; - int index; -} Iterator; - -Iterator *create_iterator(Config *cfg); -void iterator_destroy(Iterator *iter); -void iterator_next(Iterator *iter); - -#endif diff --git a/testgen/main.c b/testgen/main.c index edfd64a..7fc52df 100644 --- a/testgen/main.c +++ b/testgen/main.c @@ -1,34 +1,430 @@ #define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include #include +#include -#include "config.h" -#include "iterate.h" +#define ANSI_COLOR_RED "\x1b[31m" +#define ANSI_COLOR_RESET "\x1b[0m" -int main(int argc, char *argv[]) { - Config *cfg = create_config(argc, argv); - if (cfg == NULL) { +const char* INFILE_PATH = "testdata/cases.json"; +const char* OUTFILE_PATH = "testdata/fixtures.json"; + +static void log_err(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + fprintf(stderr, ANSI_COLOR_RED); + fprintf(stderr, "error: "); + vfprintf(stderr, fmt, args); + fprintf(stderr, ANSI_COLOR_RESET); + fprintf(stderr, "\n"); + va_end(args); +} + +static void log_info(const char* fmt, ...) { + va_list args; + va_start(args, fmt); + vfprintf(stderr, fmt, args); + fprintf(stderr, "\n"); + va_end(args); +} + +static bool validate_label(const json_t* item, json_t** label, const size_t index) { + *label = json_object_get(item, "label"); + if (!*label) { + log_err("missing required prop [%ld].label", index); + return true; + } + if (!json_is_string(*label)) { + log_err("expected prop [%ld].label to be a json string", index); + return true; + } + return false; +} + +static bool validate_args(const json_t* item, json_t** args, const size_t index) { + *args = json_object_get(item, "args"); + if (!*args) { + log_err("missing required prop [%ld].args", index); + return true; + } + if (!json_is_array(*args)) { + log_err("expected prop [%ld].args to be a json array", index); + return true; + } + size_t el_index; + json_t* el; + json_array_foreach(*args, el_index, el) { + if (!json_is_string(el)) { + log_err("expected element [%ld].args[%ld] to be a json string", index, el_index); + return true; + } + } + return false; +} + +static bool validate_opts(const json_t* item, json_t** opts, const size_t index) { + *opts = json_object_get(item, "opts"); + if (!*opts) { + log_err("missing required prop [%ld].opts", index); + return true; + } + if (*opts && !json_is_string(*opts)) { + log_err("expected prop [%ld].opts to be a json string", index); + return true; + } + return false; +} + +static bool validate_lopts(const json_t* item, json_t** lopts, const size_t index) { + *lopts = json_object_get(item, "lopts"); + if (!*lopts) { + log_err("missing required prop [%ld].lopts", index); + return true; + } + if (*lopts && !json_is_string(*lopts)) { + log_err("expected prop [%ld].lopts to be a json string", index); + return true; + } + return false; +} + +typedef struct Input { + size_t index; + json_t* label; + json_t* args; + json_t* opts; + json_t* lopts; +} Input; + +static bool validate_case(json_t* item, const size_t index, Input* input) { + if (!json_is_object(item)) { + log_err("expected element [%ld] to be a json object", index); + return true; + } + bool err = validate_label(item, &input->label, index); + if (err) { + return err; + } + err = validate_args(item, &input->args, index); + if (err) { + return err; + } + err = validate_opts(item, &input->opts, index); + if (err) { + return err; + } + err = validate_lopts(item, &input->lopts, index); + if (err) { + return err; + } + return false; +} + +typedef enum GetoptMode { + GETOPT_MODE_GNU, + GETOPT_MODE_POSIX, + GETOPT_MODE_INORDER, + GETOPT_MODE_COUNT, +} GetoptMode; + +const char* mode_prefixes[GETOPT_MODE_COUNT] = { + [GETOPT_MODE_GNU] = ":", + [GETOPT_MODE_POSIX] = "+:", + [GETOPT_MODE_INORDER] = "-:", +}; + +const char* getopt_mode_name(GetoptMode mode) { + switch (mode) { + case GETOPT_MODE_POSIX: + return "posix"; + case GETOPT_MODE_INORDER: + return "inorder"; + default: + return "gnu"; + } +} + +typedef enum GetoptFunc { + GETOPT_FUNC_GETOPT, + GETOPT_FUNC_GETOPT_LONG, + GETOPT_FUNC_GETOPT_LONG_ONLY, + GETOPT_FUNC_COUNT, +} GetoptFunc; + +const char* getopt_func_name(GetoptFunc func) { + switch (func) { + case GETOPT_FUNC_GETOPT_LONG: + return "getopt_long"; + case GETOPT_FUNC_GETOPT_LONG_ONLY: + return "getopt_long_only"; + default: + return "getopt"; + } +} + +static int lopts_count(const char* optstring) { + char* dupstr = strdup(optstring); + if (!dupstr) { + return -1; + } + + int count = 0; + char* token = strtok(dupstr, ","); + while (token) { + count++; + token = strtok(NULL, ","); + } + free(dupstr); + + return count; +} + +static struct option* alloc_lopts(const int count) { + return calloc(count + 1, sizeof(struct option)); // +1 for NULL terminator +} + +void lopts_destroy(struct option* lopts) { + if (lopts) { + for (int i = 0; lopts[i].name != NULL; i++) { + free((char*)lopts[i].name); + } + free(lopts); + } +} + +static bool parse_lopts(const char* optstring, struct option* lopts) { + char* dupstr = strdup(optstring); + if (!dupstr) { + return true; // allocation failed + } + + int i = 0; + char* token = strtok(dupstr, ","); + while (token) { + char* name = token; + int has_arg = no_argument; + + size_t len = strlen(token); + if (len > 0 && token[len - 1] == ':') { + token[len - 1] = '\0'; + has_arg = required_argument; + if (len > 1 && token[len - 2] == ':') { + token[len - 2] = '\0'; + has_arg = optional_argument; + } + } + + lopts[i].name = strdup(name); + lopts[i].has_arg = has_arg; + lopts[i].flag = NULL; + lopts[i].val = -2; + + if (!lopts[i].name) { + for (int j = 0; j < i; j++) { + free((char*)lopts[j].name); + } + free(dupstr); + return true; // allocation failed + } + + i++; + token = strtok(NULL, ","); + } + + free(dupstr); + return false; +} + +struct option* create_lopts(const char* optstring) { + if (!optstring || *optstring == '\0') { + return calloc(1, sizeof(struct option)); + } + + int count = lopts_count(optstring); + if (count < 0) { + return NULL; + } + + struct option* lopts = alloc_lopts(count); + if (!lopts) { + return NULL; + } + + if (parse_lopts(optstring, lopts)) { + lopts_destroy(lopts); + return NULL; + } + + return lopts; +} + +static bool handle_case(json_t* item, const size_t index, json_t* results_array) { + Input input = { + .label = NULL, + .args = NULL, + .opts = NULL, + .lopts = NULL, + }; + bool err = validate_case(item, index, &input); + if (err) { + return true; + } + + for (GetoptFunc func = 0; func < GETOPT_FUNC_COUNT; func++) { + for (GetoptMode mode = 0; mode < GETOPT_MODE_COUNT; mode++) { + json_t* label = json_object_get(item, "label"); + json_t* args = json_object_get(item, "args"); + json_t* opts = json_object_get(item, "opts"); + json_t* lopts = json_object_get(item, "lopts"); + + json_t* result = json_object(); + json_object_set(result, "label", label); + json_object_set_new(result, "func", json_string(getopt_func_name(func))); + json_object_set_new(result, "mode", json_string(getopt_mode_name(mode))); + json_object_set(result, "args", args); + json_object_set(result, "opts", opts); + json_object_set(result, "lopts", lopts); + + json_t* iter_array = json_array(); + json_object_set_new(result, "want_results", iter_array); + + int argc = json_array_size(args); + char** argv = calloc(argc + 1, sizeof(char*)); + if (!argv) { + return true; + } + + int i; + json_t* arg; + json_array_foreach(args, i, arg) { + argv[i] = strdup(json_string_value(arg)); + if (!argv[i]) { + for (int j = 0; j < i; j++) { + free(argv[i]); + } + free(argv); + return true; + } + } + + size_t prefix_len = strlen(mode_prefixes[mode]); + size_t opts_len = strlen(json_string_value(opts)); + char optstring[prefix_len + opts_len + 1]; + + strcpy(optstring, mode_prefixes[mode]); + strcat(optstring, json_string_value(opts)); + + struct option* longoptions = create_lopts(json_string_value(lopts)); + if (!longoptions) { + for (i = 0; i < argc; i++) { + if (argv[i]) { + free(argv[i]); + } + } + free(argv); + return true; + } + + optind = 0; + opterr = 0; + optopt = 0; + int opt; + int longindex = 0; + for (;;) { + if (func == GETOPT_FUNC_GETOPT) { + opt = getopt(argc, argv, optstring); + } + if (func == GETOPT_FUNC_GETOPT_LONG) { + opt = getopt_long(argc, argv, optstring, longoptions, &longindex); + } + if (func == GETOPT_FUNC_GETOPT_LONG_ONLY) { + opt = getopt_long_only(argc, argv, optstring, longoptions, &longindex); + } + + json_array_append_new( + iter_array, + json_pack( + "{s:i, s:i, s:i, s:s?, s:i}", + "opt", opt, + "optind", optind, + "optopt", optopt, + "optarg", optarg, + "longindex", longindex)); + + longindex = 0; + + if (opt == -1) { + json_object_set_new(result, "want_optind", json_integer(optind)); + json_t* want_args_array = json_array(); + json_object_set_new(result, "want_args", want_args_array); + for (i = 0; i < argc; i++) { + json_array_append_new(want_args_array, json_string(argv[i])); + } + + break; + } + } + + for (i = 0; i < argc; i++) { + if (argv[i]) { + free(argv[i]); + } + } + free(argv); + lopts_destroy(longoptions); + json_array_append_new(results_array, result); + } + } + + return false; +} + +int main(void) { + json_t* root; + json_error_t json_err; + + root = json_load_file(INFILE_PATH, 0, &json_err); + if (!root) { + log_err("decoding %s, %s at line %d, col %d", INFILE_PATH, json_err.text, json_err.line, json_err.column); + json_decref(root); return EXIT_FAILURE; } - Iterator *iter = create_iterator(cfg); - if (iter == NULL) { - config_destroy(cfg); + if (!json_is_array(root)) { + log_err("expected input to be a json array"); + json_decref(root); return EXIT_FAILURE; } + log_info("loaded %d cases", json_array_size(root)); + + json_t* results_array = json_array(); - iterator_next(iter); - for (; iter->status == ITER_OK; iterator_next(iter)) { - printf("[%d].label: \"%s\"\n", iter->index, iter->current.label); + size_t index; + json_t* item; + json_array_foreach(root, index, item) { + bool err = handle_case(item, index, results_array); + if (err) { + json_decref(results_array); + json_decref(root); + return EXIT_FAILURE; + } } + log_info("generated %d fixtures", json_array_size(results_array)); - if (iter->status == ITER_ERROR) { - iterator_destroy(iter); - config_destroy(cfg); + int err = json_dump_file(results_array, OUTFILE_PATH, JSON_INDENT(4)); + if (err) { + log_err("\"%s\" while opening %s", strerror(errno), OUTFILE_PATH); + json_decref(results_array); + json_decref(root); return EXIT_FAILURE; } - iterator_destroy(iter); - config_destroy(cfg); + json_decref(results_array); + json_decref(root); return EXIT_SUCCESS; }