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

Make transcripts idempotent #5406

Open
wants to merge 15 commits into
base: trunk
Choose a base branch
from
Open

Conversation

sellout
Copy link
Contributor

@sellout sellout commented Oct 10, 2024

Overview

This contains a lot of improvements to transcripts. The tl;dr is that now transcript outputs can be used as transcripts, so bug reports can contain just the output (and the output can be edited down to just the relevant bits), rather than including or attaching separate transcript and output.

Some more detail

  • all serialization is now down via CMark (previously, all api and ucm blocks in the output were still being serialized directly, because of the interleaving of the input text and command/request output)
    • previously some ucm blocks needed more backticks, but still only had three because of this, so that’s now fixed
  • transcript parser can now parse :added-by-ucm
  • transcript parser can now parse ucm and api outputs (api outputs are now indented similarly to ucm outputs to make this easier)
  • unified handling of :hide(:all), :error, and :added-by-ucm – previously each block time handled them with different semantics, including that for ucm, :hide and :hide:all had the same behavior1
  • info tags (e.g., :hide) are now preserved in the output
  • failing transcripts (but not yet parse errors) result in valid Markdown (i.e., the error is wrapped in a fenced block to preserve its formatting)
  • updates the bug_report template to encourage putting the transcript directly into the issue, rather than fencing it in a ``` markdown block

Caveats:

  • failing transcripts are not yet idempotent (but :error is)
  • :hide:all is (intentionally) not idempotent

Fixes #5199.

Implementation notes

This changes a bunch of stuff in the transcript implementation. It tries to make some things clearer (e.g., breaking awaitInput into smaller functions, and removing the recursion from it), but there is still work to do there.

Interesting/controversial decisions

This PR currently makes most of the transcript tests into “idempotent tests” – where the output is written back to the input file, and there should be no changes when re-run. It makes a nice impact with 11 kloc removed, but I don’t know that we actually want to do this.

One reason not to is that the transcripts are much noisier, since all the output is included. But maybe this should be handled by using :hide more liberally. I added a commit at the end that does that for a few transcripts.

Test coverage

Every transcript is touched by this PR, I’m pretty sure.

Loose ends

There is related work here that doesn’t need to be in this PR

  • more simplification of the transcript runner
  • dealing with parse errors better
  • probably some other things I’m forgetting

Footnotes

  1. You’ll see in the diffs a bunch of “new” ``` ucm :hide blocks. These were there before, but not copied to the output because of the bug where :hide and :hide:all were treated the same for ucm.

All serialization is now down via CMark (previously, all `api` and `ucm` blocks in the output were still being
serialized directly, because of the interleaving of the input text and command/request output).

This causes a few changes to the existing transcript outputs:
- there is now always a blank line between Markdown block elements,
- leading blank lines in UCM blocks are gone,
- blank lines at the end of transcripts are gone (they still end with a final POSIX newline), and
- some `ucm` blocks now have 4-backtick fences (because they contain triple-backticks).

Transcript failures are now also handled as Markdown, rather than just being text appended to the document. This mostly
doesn’t change the serialization, except that the failure message is now fenced, since they often contain newlines and
indentation that is mangled otherwise.
This includes changes that get us closer to running transcript outputs as inputs
- all `ProcessedBlock`s (`api`, `ucm`, and `unison`) now handle info tags the same way – previously `ucm` didn’t allow
spaces between tags like `:hide :error`, and `api` didn’t support info tags at all.
- preserve info tags in the output – `ucm :error` in the input results in `ucm :error` in the output
- parser now supports `:added-by-unison` (only useful for parsing outputs as inputs)
- fix `:hide` on `ucm` and `api` blocks – previously it behaved the same as `:hide:all`, which prevents the outputs being used
as transcripts
- include `:added-by-ucm` to `ucm` blocks inserted after `unison` blocks
- ensure output lines have 2-space indents

This adds a couple transcripts testing the `:hide` behavior.
This provides the final pieces to make transcripts idempotent enough
that we can use transcript outputs as inputs.

This simplifies bug reporting, as now the GitHub issue can be written
_as_ a transcript, even if you want to include some of the output in the
bug report. No need to wrap the entire transcript in ` ```` markdown`.

The parser changes here also improve error messages in some existing cases.

There are some caveats:
- failing transcripts are not _yet_ idempotent. There are issues with how errors are reported (especially parse
failures) that make them not round-trip
- `:hide:all` almost always breaks idempotency. This is intentional – to just hide the output, use `:hide` instead.
These were previously separated because transcripts previously couldn’t contain code blocks with triple backticks, but that is fixed now.
This produces the first output files, which should remain unchanged on
subsequent runs.
Idempotent transcripts can get large, so using `:hide` more liberally is
probably a good idea. This tries that out on a few transcripts.
@sellout
Copy link
Contributor Author

sellout commented Oct 10, 2024

Hrmm … the tests that are failing … they’re easy enough to fix (just need to update the output expectations), but it seems like there’s not an easy way to run these tests. I.e., they’re not part of ./scripts/checks.sh or anything else. Is that right? If so, we should probably add scripts for those jobs.

Copy link
Contributor

@aryairani aryairani left a comment

Choose a reason for hiding this comment

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

I'm really liking this overall, and I think @mitchellwrosen will like it too as he had previously raised concerns about :hide hiding too much.

I'm just working through the long diff now.

.github/ISSUE_TEMPLATE/bug_report.md Outdated Show resolved Hide resolved
some.ns.direct : Nat
some.ns.direct.doc : Doc2
some.ns.pretty.deeply.nested : Nat
some.ns.pretty.deeply.nested.doc : Doc2
some.outside : Nat
some.outside.doc : Doc2

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure we want to lose this line 47? But I'm not sure where all it shows up. If it's the last one in a ```, like the previous one on line 34 and like the one on line 49, then sure; but here it's bumping into the next command but without a vertical space.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, there are some differences with blank lines before and after this change. I tried to make it consistent in the code, but it definitely introduced some cases that are less than ideal. I’ll go back and review those changes.

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 thinking what you have is good, except that there should be a linebreak between result and the next prompt when there are multiple prompts in an output ucm block. (I'm also wondering whether the linebreak would missing now in an interactive ucm session?)

Copy link
Contributor

@aryairani aryairani left a comment

Choose a reason for hiding this comment

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

Can you suggest a way we can be confident that all of the transcripts made it into idempotent/ and not accidentally deleted like I'd apparently done in the past?

@sellout
Copy link
Contributor Author

sellout commented Nov 11, 2024

Can you suggest a way we can be confident that all of the transcripts made it into idempotent/ and not accidentally deleted like I'd apparently done in the past?

So I tried to split up the commits to make this more obvious, but I don’t think it’s foolproof:

  • cf0f3e2 removes all of (and only) the outputs for now-idempotent transcripts (and moves the idempotent ones to a new subdir), then
  • 73b2a9c runs everything, producing the “idempotent” versions of the transcripts.

So, basically, verify that only output files were removed in that first commit. And I think no other commits should remove any transcripts. If they do, they should be spot-checkable (and I apologize for not isolating it better).

@aryairani
Copy link
Contributor

So I tried to split up the commits to make this more obvious, but I don’t think it’s foolproof:

Ah, perfect, thanks, I'll take another look.

Checks that other transcripts in the source tree (currently just the GitHub
bug-report template) are valid.

This also fixes a bug where a transcript block like ` ``` ucm :hidec`
would be parsed like ` ``` ucm :hide c`, rather than complaining that
there is no `hidec` tag.

This currently fails because of a typo introduced earlier in this PR.
This is caught by a test introduced in the previous commit.
This also makes a couple minor changes re: running the script:

- removes the “belt and suspenders” `echo`ing that resulted in things being printed in triplicate
- added `gettext` to the Nix environment,  so `envsubst` is available
- changed the #!, to not get stuck with Bash 3.2 on macOS.
@sellout sellout marked this pull request as ready for review November 11, 2024 23:57
@sellout sellout requested a review from a team as a code owner November 11, 2024 23:57
Comment on lines -13 to -14
Input:
```` markdown
Copy link
Contributor

@aryairani aryairani Nov 12, 2024

Choose a reason for hiding this comment

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

I'm kinda thinking that we do want to continue wrapping these in ```` markdown, to make it easier to copy them out? Otherwise we have to edit source on each issue to get the contents.

Also the formatting of the internal markdown may clash with the enclosing markdown.

An upside to the change though is that it avoids Github cropping long code blocks.

Agreed we don't need Input and Output sections.

I guess either way is fine and we can always change it back later.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm kinda thinking that we do want to continue wrapping these in ```` markdown, to make it easier to copy them out? Otherwise we have to edit source on each issue to get the contents.

I was thinking it’s probably easy (for some Web person … probably not easy for me 😅) to make a bookmarklet or something to copy the raw issue description directly to the clipboard.

Longer-term, I could see a flow where you can do

scratch/main> transcript.run https://github.com/unisonweb/unison/issues/5451

  Fetching transcript from topic description.
  Saved transcript to ./unison-issue-5451.md.
  Running transcript …

possibly even firing that off from a bookmarklet or browser extension or whatever.

IMO, there are two reasons to avoid nesting in markdown:

  • reporters will write most of their report outside of the markdown block to get formatting for their text, and only put the necessary code snippets in the block, which means either more work (copy/pasting text from the issue) getting an informative transcript out of it, or just that we end up with transcripts without info (a problem that exists historically, with transcripts not containing any context, just a chunk of code that didn’t work right at some point for some reason).
  • And now I forget the second one …

Also the formatting of the internal markdown may clash with the enclosing markdown.

Do you mean that the GitHub-flavored Markdown of the issue could be incompatible with the CommonMark used for the transcript?

I suppose this is possible, but as currently specified, the two are mutually-parseable. GFM is CommonMark with some extensions, but those extensions just end up as plain text in CommonMark (e.g., strikethrough).

One other consequence is that bug_report.md and pull_request_template.md wouldn’t be templates, just contain templates, and so I would remove the transcripts/project-outputs tests.

@aryairani
Copy link
Contributor

I'm about ready to merge this, but still just trying to wrap my head around and document the new organization. Let me know if I have this right.

It looks like:

  • basically all transcripts are grouped under either errors/ or idempotent/
  • there are a handful of transcripts in the root unison-src/transcripts that either we're not trying to convert right now because they use :hide:all or like in the case of fix-5402.md I assume slipped in from trunkafter the main work was already done.
  • there are still some transcripts under transcripts-using-base/ that we don't mess with yet
  • there's project-outputs/ which includes some github templates and also any markdown from ./docs/
  • transcripts-manual/ and transcripts-round-trip/ are done currently with a different workflow, no change there
  • transcripts-using-base/ we also don't change in this PR, but might want to look at later to give a similar treatment to.

I'm kinda thinking we can eventually make in-place the primary mode for running the transcripts, but we don't have to figure that out now.

And this PR mainly is to let us reduce how many files we've got, and also to reduce friction in submitting transcripts with bug reports.

@sellout
Copy link
Contributor Author

sellout commented Nov 12, 2024

I'm about ready to merge this, but still just trying to wrap my head around and document the new organization. Let me know if I have this right.

It looks like:

  • basically all transcripts are grouped under either errors/ or idempotent/

And the errors aren’t currently handled idempotently because parse errors don’t preserve the transcript. Runtime errors do, and so those could be idempotent, but I’d rather just get the parse errors handled better, then make them all that way.

  • there are a handful of transcripts in the root unison-src/transcripts that either we're not trying to convert right now because they use :hide:all or like in the case of fix-5402.md I assume slipped in from trunkafter the main work was already done.

Correct about :hide:all, but ooof, I thought I caught ones that were added in trunk. I can make sure those are moved.

  • there are still some transcripts under transcripts-using-base/ that we don't mess with yet

  • there's project-outputs/ which includes some github templates and also any markdown from ./docs/

Yeah, I didn’t have a great name for this directory, so feel free to suggest a different one, but these are meant to validate anything transcript-y that lives in the repo for some reason other than being a test. I.e., these test that those transcripts don’t change incompatibly rather than testing that the Transcript.hs implementation is correct.

  • transcripts-using-base/ we also don't change in this PR, but might want to look at later to give a similar treatment to.

Yep – maybe we split each type of transcript tests (“basic” (which currently doesn’t have an explicit directory), “using-base”, and “errors”) into “idempotent” and “non-idempotent” subdirs, or we can dynamically identify whether each transcript uses :hide:all (this could be returned by the runner, since we write the output file all at once after running).

I'm kinda thinking we can eventually make in-place the primary mode for running the transcripts, but we don't have to figure that out now.

And this PR mainly is to let us reduce how many files we've got, and also to reduce friction in submitting transcripts with bug reports.

There is one thing here that I want to call out:

While the transcripts are idempotent (modulo parse errors & :hide:all), the first run can still discard part of a transcript. E.g.,

``` ucm :hide
scratch/main> merge.builtins
```

``` unison
This should fail to parse, but pretend it’s a typo rather than this arbitrary text.
```

``` ucm
scratch/main> add
```

results in this output

``` ucm :hide
scratch/main> builtins.merge
```

``` unison
This should fail to parse, but pretend it’s a typo rather than this arbitrary text.
```

🛑

The transcript failed due to an error in the stanza above. The error is:

``` 
I found a ',' here, but I didn't see a list or tuple that it might be a separator for.

    1 | This should fail to parse, but pretend it’s a typo rather than this arbitrary text.
```

So, running it in place by default can be annoying when writing a new transcript. What I currently do is stage my transcript, then run it and if anything is lost, just discard the unstaged changes.

In future, transcript runs could be “non-destructive” – i.e., make sure that all of the original stanzas are included in the output, even if there was an error earlier in the run.

@aryairani
Copy link
Contributor

aryairani commented Nov 12, 2024

There is one thing here that I want to call out: While the transcripts are idempotent (modulo parse errors & :hide:all), the first run can still discard part of a transcript.

Ooo, okay. A little dangerous. So I guess for now if you put it under idempotent/, you'll run that risk that an unexpected error could clobber the rest of the transcript. So instead you need to put it in transcripts/ or run it manually until you know it works, and then move it to idempotent later. Or stage it like you mentioned.

@sellout
Copy link
Contributor Author

sellout commented Nov 12, 2024

And this PR mainly is to let us reduce how many files we've got, and also to reduce friction in submitting transcripts with bug reports.

Yeah, I would say “better bug reports” are the main thing here.

What I like to do is run my transcript, paste the output.md into the issue description, then edit the output in the UCM blocks. There can be a lot of output and :hide (while definitely useful) can be a bit coarse. So I could have a bug report like (NB: This is not a real bug)

I can get the help for `alias.type`

``` ucm
scratch/main> help alias.type

  alias.type
  `alias.type Foo Bar` introduces `Bar` with the same definition as `Foo`.
```

but when I run just `help`, that command is missing 

``` ucm
scratch/main> help

  ...
  alias.many (or copy)
  `alias.many <relative1> [relative2...] <namespace>` creates aliases `relative1`, `relative2`, ...
  in the namespace `namespace`.
  `alias.many foo.foo bar.bar .quux` creates aliases `.quux.foo.foo` and `.quux.bar.bar`.
  
  alias.term
  `alias.term foo bar` introduces `bar` with the same definition as `foo`.
  
  api
  `api` provides details about the API.
  
  auth.login
  Obtain an authentication session with Unison Share.
  `auth.login`authenticates ucm with Unison Share.
  ...
```

where I’ve trimmed 90+% of the help output to just focus on the problem, but without having to also include a separate input transcript, which breaks the flow of the report (and also results in context-free transcripts when that chunk is copy/pasted from the issue on its own).

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.

Make transcript output runnable as a transcript
2 participants