Commit 616051d2 authored by Eric Myhre's avatar Eric Myhre

Emit multiple packages in codegen tests. Exericse as plugins.

Using golang's plugin feature, we can... well, *do* this.

To date, testing codegen has involved running the "test" in the gen
package to get it to emit code; and then switching to the emitted
package and _manually_ running the tests there.

Now, running `go test` in the gen package is sufficient to do
*everything*: both the generation, and the result compiling,
and we can even write tests against the interfaces and run those,
all in one step.

There's also lots of stuff that becomes possible now that we can easily
generate multiple separate packages with various codegen outputs:

- Overall: everything is granular.  We can test selections of things,
  rather than needing to have everything fall into place at once.
- Generally more organized results.
- We can more easily inspect the size of generated code.
- We can more easily inspect the size of the compiled result of gen!
  (Okay, not really.  I'm seeing a '.so' file that's 4MB is coming out
  from 200sloc of "String".  I don't think that's exactly informative.
  Some constant factor is thoroughly obscuring the data of interest.
  Nice idea in theory though, and maybe we'll get there later.)
- We can diff the generated type for variations in adjunct config!
  (Probably not something that will end up tested, but neat to be able
  to do during dev.)

Doing this with go plugins seemed like the neatest way to do this.
It's certainly not the only way, though.  And in particular, I will
confess that this will probably make developing from a windows host
pretty painful: go plugins aren't supported on windows.  Mind,
this doesn't mean you can't *use* codegen or its results on windows.
It just means the tests won't work.  So, someone doing development
_on the codegen itself_ would have to wait for the CI server to run
the tests on their behalf.  Hopefully this is survivable.

(There's also a fun wee wiggle in that loading a plugin has the
requirement that it be built with the same "runtime".  The definition
of "runtime" here happens to include whether or not things have been
built in "race" mode.  So, the '-race' flag disappears from our CI
config file in this diff; otherwise, CI will get the (rather confusing)
error "plugin was built with a different version of package runtime".
This isn't really worrying to ditch, though.  I'm... not even sure why
the '-race' was in our CI script in the first place.  Must've arrived
via cargo cult; we don't _have_ any concurrency in this library.)

An alternative way to go about all this would be to have the tests for
gen invoke `go test` (rather than `go build` in plugin mode) on each of
the generated packages.  It strikes me as similar but worse.
We still have to invoke the go tools from inside the test;
we'd have *more* work to do to either copy tests into the gen'd package
or else generate calls back to the parent package for test functions
(which still have to be written against interfaces, so that they can
compile even when the gen isn't done, as it indeed isn't when you
freshly check out the repo -- exact same as with the plugin approach);
and in exchange for the extra work, we get markedly worse output
('go test' doesn't nest nicely, afaict), and we can't separate the
compiling of the generated code from the evaluation of tests on it,
and we'd have no possibility of passing information via closures should
we wish to develop table-driven tests where this would be useful.
tl;dr: Highest cost, uglier, and worse.

No matter which way we go about this, there *is* a speed trade-off.
Invoking the compiler per package adds at least a half second of time
for each package, in practice.  Worth it, though.

And on the off chance that this plugin approach does burn later,
and we do want to switch to child process 'go test' invocations...
the good news is: we shouldn't actually have to rewrite very much.
The everything-starts-from-NodeStyle-and-tests-against-Node work is
exactly the same for making the plugin approach work, and will
trivially pivot to working fine in for child 'go test' approaches,
should we find it necessary to do so in the future.  So!  With our
butts covered: a plugin'ing we shall go!

Some of the code here still needs cleanup; this is a proof of concept
checkpointing commit.  (The real thing probably won't have such
function names as "TestFancier".)  But, we do get to see here:
plugins work; more than one in the process works; and they work even
when the same type names are in the generated packages.  All good.
parent 5f93c298
......@@ -19,7 +19,7 @@ before_script:
- go test -run xxxx ./...
script:
- go test -race -short -coverprofile=coverage.txt ./...
- go test -short -coverprofile=coverage.txt ./...
after_success:
- bash <(curl -s https://codecov.io/bash)
package gengo
import (
"io"
"os"
"os/exec"
"path/filepath"
"plugin"
"testing"
"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/schema"
)
func invokeBuildPlugin(prefix string) {
cmd := exec.Command("go", "build", "-o=./_test/"+prefix+"/obj.so", "-buildmode=plugin", "./_test/"+prefix)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
panic(err)
}
}
func loadPlugin(prefix string) *plugin.Plugin {
plg, err := plugin.Open("./_test/" + prefix + "/obj.so")
if err != nil {
panic(err)
}
return plg
}
func withFile(filename string, fn func(io.Writer)) {
// Rm-rf the whole "./_test" dir at your leasure.
// We don't by default because it's nicer to let go's builds of things cache.
// If you change the names of types, though, you'll have garbage files leftover,
// and that's currently a manual cleanup problem. Sorry.
os.Mkdir(filepath.Dir("./_test/"), 0755)
os.Mkdir(filepath.Dir("./_test/"+filename), 0755)
f, err := os.OpenFile("./_test/"+filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
panic(err)
}
fn(f)
}
func genAndCompilerAndTest(
t *testing.T,
prefix string,
pkgName string,
ts schema.TypeSystem,
adjCfg *AdjunctCfg,
tests func(t *testing.T, getStyleByName func(string) ipld.NodeStyle),
) {
t.Run("generate", func(t *testing.T) {
// Emit fixed bits.
withFile(prefix+"/minima.go", func(f io.Writer) {
EmitInternalEnums(pkgName, f)
})
// Emit a file for each type.
for _, typ := range ts.GetTypes() {
withFile(prefix+"/t"+typ.Name().String()+".go", func(f io.Writer) {
EmitFileHeader(pkgName, f)
switch t2 := typ.(type) {
case schema.TypeString:
EmitEntireType(NewStringReprStringGenerator(pkgName, t2, adjCfg), f)
}
})
}
// Emit an exported top level function for getting nodestyles.
// (This part isn't necessary except for a special need we have with this plugin trick;
// normally, user code uses the `{pkgname}.Style.{TypeName}` constant access.)
withFile(prefix+"/styleGetter.go", func(w io.Writer) {
doTemplate(`
package `+pkgName+`
import "github.com/ipld/go-ipld-prime"
func GetStyleByName(name string) ipld.NodeStyle {
switch name {
{{- range . }}
case "{{ .Name }}":
return _{{ . | TypeSymbol }}__Style{}
case "{{ .Name }}.Repr":
return _{{ . | TypeSymbol }}__ReprStyle{}
{{- end}}
default:
return nil
}
}
`, w, adjCfg, ts.GetTypes())
})
t.Run("compile", func(t *testing.T) {
invokeBuildPlugin(prefix)
plg := loadPlugin(prefix)
sym, err := plg.Lookup("GetStyleByName")
if err != nil {
panic(err)
}
getStyleByName := sym.(func(string) ipld.NodeStyle)
t.Run("test", func(t *testing.T) {
tests(t, getStyleByName)
})
})
})
}
func TestFancier(t *testing.T) {
ts := schema.TypeSystem{}
ts.Init()
adjCfg := &AdjunctCfg{
maybeUsesPtr: map[schema.TypeName]bool{},
}
ts.Accumulate(schema.SpawnString("String"))
adjCfg.maybeUsesPtr["String"] = false
prefix := "foo"
pkgName := "main" // has to be 'main' for plugins to work. this stricture makes little sense to me, but i didn't write the rules.
genAndCompilerAndTest(t, prefix, pkgName, ts, adjCfg, func(t *testing.T, getStyleByName func(string) ipld.NodeStyle) {
ns := getStyleByName("String")
t.Run("string operations work", func(t *testing.T) {
nb := ns.NewBuilder()
nb.AssignString("woiu")
n := nb.Build()
t.Logf("%v\n", n)
})
t.Run("null is rejected", func(t *testing.T) {
nb := ns.NewBuilder()
nb.AssignNull()
})
})
}
func TestFanciest(t *testing.T) {
ts := schema.TypeSystem{}
ts.Init()
adjCfg := &AdjunctCfg{
maybeUsesPtr: map[schema.TypeName]bool{},
}
ts.Accumulate(schema.SpawnString("String"))
adjCfg.maybeUsesPtr["String"] = true
prefix := "bar"
pkgName := "main" // has to be 'main' for plugins to work. this stricture makes little sense to me, but i didn't write the rules.
genAndCompilerAndTest(t, prefix, pkgName, ts, adjCfg, func(t *testing.T, getStyleByName func(string) ipld.NodeStyle) {
ns := getStyleByName("String")
t.Run("string operations work", func(t *testing.T) {
nb := ns.NewBuilder()
nb.AssignString("woiu")
n := nb.Build()
t.Logf("%v\n", n)
})
t.Run("null is rejected", func(t *testing.T) {
nb := ns.NewBuilder()
nb.AssignNull()
})
})
}
......@@ -69,7 +69,13 @@ func (stringReprStringReprGenerator) EmitNodeMethodAsString(io.Writer) {}
func (stringReprStringReprGenerator) EmitNodeMethodAsBytes(io.Writer) {}
func (stringReprStringReprGenerator) EmitNodeMethodAsLink(io.Writer) {}
func (stringReprStringReprGenerator) EmitNodeMethodStyle(io.Writer) {}
func (stringReprStringReprGenerator) EmitNodeStyleType(io.Writer) {}
func (g stringReprStringReprGenerator) EmitNodeStyleType(w io.Writer) {
// Since this is a "natural" representation... there's just a type alias here.
// No new functions are necessary.
doTemplate(`
type _{{ .Type | TypeSymbol }}__ReprStyle = _{{ .Type | TypeSymbol }}__Style
`, w, g.AdjCfg, g)
}
func (g stringReprStringReprGenerator) GetNodeBuilderGenerator() NodeBuilderGenerator {
return stringReprStringReprBuilderGenerator{g.AdjCfg, g.Type}
}
......
......@@ -51,3 +51,18 @@ func SpawnStructField(name string, typ Type, optional bool, nullable bool) Struc
func SpawnStructRepresentationMap(renames map[string]string) StructRepresentation_Map {
return StructRepresentation_Map{renames, nil}
}
// The methods relating to TypeSystem are also mutation-heavy and placeholdery.
func (ts *TypeSystem) Init() {
ts.namedTypes = make(map[TypeName]Type)
}
func (ts *TypeSystem) Accumulate(typ Type) {
ts.namedTypes[typ.Name()] = typ
}
func (ts TypeSystem) GetTypes() map[TypeName]Type {
return ts.namedTypes
}
func (ts TypeSystem) TypeByName(n string) Type {
return ts.namedTypes[TypeName(n)]
}
......@@ -6,6 +6,8 @@ import (
type TypeName string // = ast.TypeName
func (tn TypeName) String() string { return string(tn) }
// typesystem.Type is an union interface; each of the `Type*` concrete types
// in this package are one of its members.
//
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment