Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suite feature #16

Merged
merged 4 commits into from
May 10, 2023
Merged

Suite feature #16

merged 4 commits into from
May 10, 2023

Conversation

warpfork
Copy link
Owner

@warpfork warpfork commented May 1, 2023

This addresses several sources of recurring boilerplate that I'm distilling from multiple experiences of using testmark in practice, and also seeks to address several common footgun issues. especially around detecting whether or not fixtures are actually used.

With this, you are sometimes required to write a more complex TestingPattern.Run function, but in exchange, you get vastly less boilerplate about file loading, parsing, index buildilng, and hunk walking, as well as a great number of guardrails such as warnings about unused hunks, and your overall setup can become very declarative:

For example, the declarative top-level wiring may now look like:

func TestCommandDocs(t *testing.T) {
    suite := sweet.NewSuiteManager(osfs.DirFS(fixtureDir))
    suite.MustWorkWith("fixture-*.md", "testcase/*", customTestingPattern{})
    suite.MustWorkWith("demo-*.md, "*", testexec.TestingPattern{...})
    suite.Run(t)
}

And hopefully we can make more reusable TestingPattern APIs appear throughout other extensions as well (such as testexec). This should make such extensions much easier to wire up, and much easier to combine with other tests or extensions.

@warpfork
Copy link
Owner Author

warpfork commented May 1, 2023

An example of a custom testing pattern looks like this:

type testingPattern struct{}

func (tp testingPattern) Name() string          { return "cli doc test" }
func (tp testingPattern) OwnsAllChildren() bool { return false }
func (tp testingPattern) Run(
	t *testing.T,
	filename string,
	subject *testmark.DirEnt,
	reportUse func(string),
	reportUnrecog func(string),
	patchAccum *testmark.PatchAccumulator,
) error {
	reportUse(subject.Path)
	command := strings.Split(strings.Split(filename, ".md")[0], "-")
	var buf bytes.Buffer
	mycli.App.Writer = &buf
	mycli.App.ErrWriter = &buf
	mycli.App.Run(append(command, "-h"))
	if patchAccum != nil {
		newHunk := *subject.Hunk
		newHunk.Body = buf.Bytes()
		patchAccum.AppendPatch(newHunk)
		return nil
	}
	quicktest.Assert(t, buf.String(), quicktest.Equals, string(subject.Hunk.Body))
	return nil
}

This happens to be (real!) code appropriate for wiring up a certain CLI library to generate its help text and use that as a fixture, so some of the errata in the function body concerns that.

There's still a decent amount of code here -- partially because of the increased complexity of having the test function report for itself which hunks it has used! -- but drastically less than you'd have without using the suite system.

We can also see that some references to the patch accumulator system remain fairly manual. I think this is natural and correct. Sometimes fixture regeneration involves updating exactly one hunk (like this case does); sometimes it's much more complicated (such as in testexec, which may update three different hunks at once!), and so this can't really be reduced any further. Compared to without using the suite system: you also would've had to repeat the filename multiple times, for loading the first time, and later for opening writably to patch, and so by using the suite system you're still saving quite a bit of boilerplate over the prior situation... as well as having the suite manager aggregate all patches to the same file for you, if more than one testing pattern is working on hunks in the same file, which is a nice improvement.

reportUse func(string), // Should be called with the full path of any hunk that's consumed by this test. Used to detect orphaned hunks that went unused by the whole suite.
reportUnrecog func(string), // If this test code owns all child hunks, it may call this to report one that it doesn't recognize.
patchAccum *testmark.PatchAccumulator, // If non-nil, means regenerating golden master data is requested instead of testing.
) error // Run may return errors or call t.Fatal itself.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm uncertain this error return makes any sense. I lean towards including an error return when not sure, but, so far when implementing this, I've always just had return nil.

The labeling performed when you return an error might be helpful, but... everything in that message is available in other ways:

  • the filename is already in the t.Run name tree
  • the hunk name is also already in the t.Run name tree
  • the tp.Name() isn't, but... the line number in the t.Log output will point at its guts anyway.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with it returning an error. testing.T.Run returns a bool and it seems more idiomatic to return an error than a bool.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but the callback parameter to testing.T.Run, which is the more apt comparison, is func(*T) with no return value.

I think I'll still side with you in keeping the error return though, just because as a heuristic, almost any interface that doesn't return errors seems to surprise and frustrate me in the long run, much more often than not. And even if I'm wrong, foisting a "return nil" onto implementers isn't a very big burden.

suite/suite.go Outdated Show resolved Hide resolved
@warpfork
Copy link
Owner Author

@TripleDogDare and I had a little additional review discussion about this outside of github, which I'll replay some notes from:

Mostly, wondered if that Run function interface can get any shorter. And in particular, if the signature of those "report*" funcs -- both func(string) -- are... well, okay.

And we're hovering around "yeah, it's fine".

We don't love the size and verbosity of that function interface, but the only alternatives we can think of are bundling two or more of those parameters into another object that we export from this suite package. Doing so results in:

  • neutral: no significant reduction the amount of code either here for for implementers.
  • positive: we would gain the ability to add more accessors in the future without it being a breaking change.
    • ... but we could also switch to this by adding variations of the TestingPattern interface to what we support in this package, which is also pretty graceful.
  • negative: forces all implementors to import this package.
  • positive (mild): name consistency in implementers is forced by accessor func names (vs arg names force nothing).

... which seems to sum to overall to pretty much a wash.

@warpfork
Copy link
Owner Author

I admit I gave a little more thought yet to if "adding more accessors without it being a breaking change" thing should be the dominant consideration, but I think I'm gonna halt that train of thought and get on with merging this.

If we start introducing features that make this package less optional, then we might as well move the suite feature to the core, and then that can be the time to revisit the API with whatever we've learned in the meanwhile from usage in the wild.

@warpfork
Copy link
Owner Author

I haven't updated the textexec package to interact with this yet. I think it should be pretty easy to do so, without too much backwards compatibility break there, either -- we can just add the report func callbacks to its configuration struct, and then all the interactions can be wired together with that, without any strong dependencies in either direction.

warpfork and others added 2 commits May 10, 2023 13:19
This addresses several sources of recurring boilerplate that I'm
distilling from multiple experiences of using testmark in practice,
and also seeks to address several common footgun issues. especially
around detecting whether or not fixtures are actually used.

With this, you are sometimes required to write a more complex
TestingPattern.Run function, but in exchange, you get vastly
less boilerplate about file loading, parsing, index buildilng,
and hunk walking, as well as a great number of guardrails
such as warnings about unused hunks, and your overall setup
can become very declarative:

For example, the declarative top-level wiring may now look like:

    func TestCommandDocs(t *testing.T) {
        suite := sweet.NewSuiteManager(osfs.DirFS(fixtureDir))
        suite.MustWorkWith("fixture-*.md", "testcase/*", customTestingPattern{})
        suite.MustWorkWith("demo-*.md, "*", testexec.TestingPattern{...})
        suite.Run(t)
    }

And hopefully we can make more reusable TestingPattern APIs
appear throughout other extensions as well (such as testexec).
This should make such extensions much easier to wire up,
and much easier to combine with other tests or extensions.

This introduces a dependency on the go-fsx package, which adds
writablity to the features we can expect of an FS interface.
Avoiding this doesn't seem worth additional effort.
(I would be happy to drop it the instant that stdlib adds
equivalent features.)
This replaces the strict mode feature.
If you use the suite style, you get strict mode.

(We could re-introduce a piece of config that causes testexec to claim
use for all child hunks, or at least to decline to report any
explicitly unrecognized hunks, but today, I don't feel inclined.
I don't think use of that feature has been above about zero.)

Removed many logf statements.  Successful tests should be quiet.

Switched quite a few errors that were previously conditional back
to immediate fatalf, where they should be.  Others disappeared
into becoming part of the suite report hook system.

For the testing of this thing itself: I don't love the `RunFailTest`
thing we've got going on here, but I see why it's a stand-in for
mocking the entirety of `*testing.T`, and I'm not gonna fight with
that today.  If you do run the new test with that flag (or just
the condition commented out), you'll indeed see nice errors from
the suite system about unused and unrecognized hunks.
@warpfork
Copy link
Owner Author

Okay, I snuck updates to the testexec package into this change set too. It indeed wasn't too bad.

It did cause one more change to the suite API, though: the reportUnrecog function also now takes another string param for a descriptive reason message.

Also change the name of "TestingPattern" to "TestingFunctor", because
I noticed I used the word "pattern" to describe both globs as well
as the callback interface, and that's quite confusing.
@warpfork
Copy link
Owner Author

... I'll continue wondering if this API is perfect in the future 🙃 It's already valuable enough that I really want to start using it downstream, so it's merge time 😆

@warpfork warpfork merged commit a04c34b into master May 10, 2023
@warpfork warpfork deleted the suite-feature branch May 10, 2023 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants