Skip to content

Conversation

@nbloomf
Copy link
Member

@nbloomf nbloomf commented Dec 13, 2025

Description

Addresses tweag/cardano-conformance-testing-of-consensus#61.
Depends on #17.

This PR adds a new option --interactive to the conformance-test-viewer tool. When present, the tool enters a primitive REPL that does the following:

  • Print the selected test case.
  • Navigate to the parent, left and right sibling, and first child of the current node in the shrink tree, using the arrow keys (Left, Up, Down, and Right respectively), and print a diff of the old and new nodes.
  • Quit by pressing q.

The input must be supplied by flag in this mode, since we need to use stdin to read online input.

Here is an example. Suppose the file test.json contains "hello world". An interactive session where I press Right, Down, Down, Right, Down looks like this:

cabal run cardano-node:conformance-test-viewer -- --interactive --input=tmp/test.json
"hello world"
[]
-"hello world" +""
[0]
-"" +" world"
[1]
-" world" +"hellod"
[2]
-"hellod" +""
[2,0]
-"" +"lod"
[2,1]

Diffing is supplied by the ToExpr class from quickcheck-state-machine, which can be derived generically.

Checklist

  • Commit sequence broadly makes sense and commits have useful messages
  • New tests are added if needed and existing tests are updated. These may include:
    • golden tests
    • property tests
    • roundtrip tests
    • integration tests
      See Runnings tests for more details
  • Any changes are noted in the CHANGELOG.md for affected package
  • The version bounds in .cabal files are updated
  • CI passes. See note on CI. The following CI checks are required:
    • Code is linted with hlint. See .github/workflows/check-hlint.yml to get the hlint version
    • Code is formatted with stylish-haskell. See .github/workflows/stylish-haskell.yml to get the stylish-haskell version
    • Code builds on Linux, MacOS and Windows for ghc-9.6 and ghc-9.12
  • Self-reviewed the diff

Note on CI

If your PR is from a fork, the necessary CI jobs won't trigger automatically for security reasons.
You will need to get someone with write privileges. Please contact IOG node developers to do this
for you.

@nbloomf nbloomf self-assigned this Dec 13, 2025
Copy link
Collaborator

@isovector isovector left a comment

Choose a reason for hiding this comment

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

I really love the idea, but am not sold on the implementation. In particular, it seems like we should reuse the ShrinkIndex combinators rather than having a parallel implementation in Data.RoseTree.

Comment on lines +274 to +281
'\ESC' -> do
c2 <- getChar
c3 <- getChar
case (c2, c3) of
('[', 'A') -> pure ToLeftSibling
('[', 'B') -> pure ToRightSibling
('[', 'C') -> pure ToFirstChild
('[', 'D') -> pure ToParent
Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess you're parsing ansi codes for arrows here? But a comment would go a long way!

| Quit
deriving (Eq, Show)

readCommand :: IO Command
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can this instead have type

Suggested change
readCommand :: IO Command
readCommand :: String -> [Command]

?

Laziness will allow us to stream it off stdin, which then means we can drop the unnecessary IO.

Comment on lines +262 to +263
= NoOp
| ToParent
Copy link
Collaborator

Choose a reason for hiding this comment

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

How about removing this constructor

Suggested change
= NoOp
| ToParent
= ToParent

and then use Maybe Command instead? Then we get the whole bevy of Maybe combinators like catMaybes or a foldable instance that automatically ignores the no-ops.

let allShrinks = makeRoseTreeZipper $ applyRoseTreeWithIndex (,) $ makeRoseTree (\x -> (node x, branches x)) $ tree
interactWithShrinks allShrinks opts Nothing

interactWithShrinks
Copy link
Collaborator

Choose a reason for hiding this comment

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

This function is really, really cool!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Agree!

Comment on lines +114 to +116
ToParent -> case toParent zipper of
Just zipper' -> interactWithShrinks zipper' opts Nothing
Nothing -> interactWithShrinks zipper opts $ Just "already at the root!"
Copy link
Collaborator

Choose a reason for hiding this comment

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

This pattern is crying out for a helper.

withZipper :: (Zipper -> Maybe Zipper) -> String -> Zipper -> ...

and then:

Suggested change
ToParent -> case toParent zipper of
Just zipper' -> interactWithShrinks zipper' opts Nothing
Nothing -> interactWithShrinks zipper opts $ Just "already at the root!"
ToParent -> withZipper toParent "already at the root" zipper

interactWithShrinks allShrinks opts Nothing

interactWithShrinks
:: (Show a, ToExpr a) => RoseTreeZipper ([Int], a) -> Options -> Maybe String -> IO ()
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this function can/should be decomposed into three pieces:

  1. folding Commands into state updates on the zipper
  2. generating output
  3. some IO plumbing to make it all work

As a general rule, the more code you can move outside of IO the better. Which makes this a prime candidate.




data RoseTree a = RoseTree
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is identical to our ShrinkTree!

Comment on lines +19 to +20
makeRoseTree decompose x = case decompose x of
(a, bs) -> RoseTree a $ fmap (makeRoseTree decompose) bs
Copy link
Collaborator

Choose a reason for hiding this comment

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

My interpretation of this can't-fail unipattern match is to force the decompose x thunk? If that's on purpose, please document what's going on. If not, I think a let binding would be more idiomatic (and thus avoid me wondering).

} deriving (Eq, Show, Functor, Foldable, Traversable)

data RoseTreeZipper a
= RoseTreeZipper (RoseTree a) [RoseTreeCtx a] (Maybe a)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please add some per-field haddock (and maybe record names) to document what each of these is.

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Data.RoseTree where
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm wondering what this module buys us. It seems like we can already do all of these operations via algebra on the ShrinkIndex, and manifest the results via lookup. IMO such would be much more preferable, since it would reuse the logic.

If your concerns are repeatedly paying the lookup costs, we could use MemoTrie to automatically memoize it.

Copy link
Collaborator

@ninioArtillero ninioArtillero left a comment

Choose a reason for hiding this comment

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

I like this new interactive mode. A neat way of exploring a shrinking strategy :) I approve this, as the few concerns I had were already pointed out in @isovector's review: primary wondering that would be better to use the existing shrink index machinery, but I suspect it could be argued someway (which I'm open to).

@dpulls
Copy link

dpulls bot commented Dec 17, 2025

🎉 All dependencies have been resolved !

Base automatically changed from add/shrink-view-tool to conformance-testing December 17, 2025 16:34
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.

4 participants