Skip to content

Commit

Permalink
Merge pull request #5394 from sellout/transcript-known-failures
Browse files Browse the repository at this point in the history
Add support for “known failures” to transcripts
  • Loading branch information
aryairani authored Dec 11, 2024
2 parents 271c105 + c88d9fd commit b311a29
Show file tree
Hide file tree
Showing 37 changed files with 317 additions and 98 deletions.
6 changes: 5 additions & 1 deletion unison-cli/src/Unison/Codebase/Transcript.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
-- | The data model for Unison transcripts.
module Unison.Codebase.Transcript
( ExpectingError,
HasBug,
ScratchFileName,
Hidden (..),
UcmLine (..),
Expand All @@ -25,6 +26,8 @@ import Unison.Project (ProjectAndBranch)

type ExpectingError = Bool

type HasBug = Bool

type ScratchFileName = Text

data Hidden = Shown | HideOutput | HideAll
Expand Down Expand Up @@ -56,13 +59,14 @@ type Stanza = Either CMark.Node ProcessedBlock
data InfoTags a = InfoTags
{ hidden :: Hidden,
expectingError :: ExpectingError,
hasBug :: HasBug,
generated :: Bool,
additionalTags :: a
}
deriving (Eq, Ord, Read, Show)

defaultInfoTags :: a -> InfoTags a
defaultInfoTags = InfoTags Shown False False
defaultInfoTags = InfoTags Shown False False False

-- | If the `additionalTags` form a `Monoid`, then you don’t need to provide a default value for them.
defaultInfoTags' :: (Monoid a) => InfoTags a
Expand Down
48 changes: 31 additions & 17 deletions unison-cli/src/Unison/Codebase/Transcript/Parser.hs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import Data.Char qualified as Char
import Data.Text qualified as Text
import Text.Megaparsec qualified as P
import Text.Megaparsec.Char qualified as P
import Unison.Codebase.Transcript hiding (expectingError, generated, hidden)
import Unison.Codebase.Transcript hiding (expectingError, generated, hasBug, hidden)
import Unison.Prelude
import Unison.Project (fullyQualifiedProjectAndBranchNamesParser)

Expand All @@ -50,9 +50,9 @@ formatStanzas =

processedBlockToNode :: ProcessedBlock -> CMark.Node
processedBlockToNode = \case
Ucm tags cmds -> mkNode (\() -> "") "ucm" tags $ foldr ((<>) . formatUcmLine) "" cmds
Unison tags txt -> mkNode (maybe "" (" " <>)) "unison" tags txt
API tags apiRequests -> mkNode (\() -> "") "api" tags $ foldr ((<>) . formatAPIRequest) "" apiRequests
Ucm tags cmds -> mkNode (\() -> Nothing) "ucm" tags $ foldr ((<>) . formatUcmLine) "" cmds
Unison tags txt -> mkNode id "unison" tags txt
API tags apiRequests -> mkNode (\() -> Nothing) "api" tags $ foldr ((<>) . formatAPIRequest) "" apiRequests
where
mkNode formatA lang = CMarkCodeBlock Nothing . formatInfoString formatA lang

Expand Down Expand Up @@ -98,20 +98,28 @@ apiRequest =
<|> APIComment <$> (P.chunk "--" *> restOfLine)
<|> APIResponseLine <$> (P.chunk " " *> restOfLine <|> "" <$ P.single '\n' <|> "" <$ P.chunk " \n")

formatInfoString :: (a -> Text) -> Text -> InfoTags a -> Text
formatInfoString :: (a -> Maybe Text) -> Text -> InfoTags a -> Text
formatInfoString formatA language infoTags =
let infoTagText = formatInfoTags formatA infoTags
in if Text.null infoTagText then language else language <> " " <> infoTagText

formatInfoTags :: (a -> Text) -> InfoTags a -> Text
formatInfoTags formatA (InfoTags hidden expectingError generated additionalTags) =
formatHidden hidden <> formatExpectingError expectingError <> formatGenerated generated <> formatA additionalTags
formatInfoTags :: (a -> Maybe Text) -> InfoTags a -> Text
formatInfoTags formatA (InfoTags hidden expectingError hasBug generated additionalTags) =
Text.intercalate " " $
catMaybes
[ formatHidden hidden,
formatExpectingError expectingError,
formatHasBug hasBug,
formatGenerated generated,
formatA additionalTags
]

infoTags :: P a -> P (InfoTags a)
infoTags p =
InfoTags
<$> lineToken hidden
<*> lineToken expectingError
<*> lineToken hasBug
<*> lineToken generated
<*> p
<* P.single '\n'
Expand All @@ -135,26 +143,32 @@ lineToken p = p <* nonNewlineSpaces
nonNewlineSpaces :: P ()
nonNewlineSpaces = void $ P.takeWhileP Nothing (\ch -> ch == ' ' || ch == '\t')

formatHidden :: Hidden -> Text
formatHidden :: Hidden -> Maybe Text
formatHidden = \case
HideAll -> ":hide:all"
HideOutput -> ":hide"
Shown -> ""
HideAll -> pure ":hide-all"
HideOutput -> pure ":hide"
Shown -> Nothing

hidden :: P Hidden
hidden =
(HideAll <$ word ":hide:all")
(HideAll <$ word ":hide-all")
<|> (HideOutput <$ word ":hide")
<|> pure Shown

formatExpectingError :: ExpectingError -> Text
formatExpectingError = bool "" ":error"
formatExpectingError :: ExpectingError -> Maybe Text
formatExpectingError = bool Nothing $ pure ":error"

expectingError :: P ExpectingError
expectingError = isJust <$> optional (word ":error")

formatGenerated :: ExpectingError -> Text
formatGenerated = bool "" ":added-by-ucm"
formatHasBug :: HasBug -> Maybe Text
formatHasBug = bool Nothing $ pure ":bug"

hasBug :: P HasBug
hasBug = isJust <$> optional (word ":bug")

formatGenerated :: ExpectingError -> Maybe Text
formatGenerated = bool Nothing $ pure ":added-by-ucm"

generated :: P Bool
generated = isJust <$> optional (word ":added-by-ucm")
Expand Down
93 changes: 64 additions & 29 deletions unison-cli/src/Unison/Codebase/Transcript/Runner.hs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ run isTest verbosity dir codebase runtime sbRuntime nRuntime ucmVersion baseURL
currentTags <- newIORef Nothing
isHidden <- newIORef Shown
allowErrors <- newIORef False
expectFailure <- newIORef False
hasErrors <- newIORef False
mBlock <- newIORef Nothing
let patternMap = Map.fromList $ (\p -> (patternName p, p) : ((,p) <$> aliases p)) =<< validInputs
Expand Down Expand Up @@ -204,12 +205,25 @@ run isTest verbosity dir codebase runtime sbRuntime nRuntime ucmVersion baseURL
-- We shorten the terminal width, because "Transcript" manages a 2-space indent for output lines.
Pretty.toPlain (terminalWidth - 2) line

maybeDieWithMsg :: String -> IO ()
maybeDieWithMsg :: Pretty.Pretty Pretty.ColorText -> IO ()
maybeDieWithMsg msg = do
errOk <- readIORef allowErrors
if errOk
then writeIORef hasErrors True
else dieWithMsg msg
liftIO $ writeIORef hasErrors True
liftIO (liftA2 (,) (readIORef allowErrors) (readIORef expectFailure)) >>= \case
(False, False) -> liftIO . dieWithMsg $ Pretty.toPlain terminalWidth msg
(True, True) -> do
appendFailingStanza
fixedBug out $
Text.unlines
[ "The stanza above marked with `:error :bug` is now failing with",
"",
"```",
Text.pack $ Pretty.toPlain terminalWidth msg,
"```",
"",
"so you can remove `:bug` and close any appropriate Github issues. If the error message is different \
\from the expected error message, open a new issue and reference it in this transcript."
]
(_, _) -> pure ()

apiRequest :: APIRequest -> IO [APIRequest]
apiRequest req = do
Expand All @@ -220,9 +234,13 @@ run isTest verbosity dir codebase runtime sbRuntime nRuntime ucmVersion baseURL
APIComment {} -> pure $ pure req
GetRequest path ->
either
(([] <$) . maybeDieWithMsg . show)
(([] <$) . maybeDieWithMsg . Pretty.string . show)
( either
(([] <$) . maybeDieWithMsg . (("Error decoding response from " <> Text.unpack path <> ": ") <>))
( ([] <$)
. maybeDieWithMsg
. (("Error decoding response from " <> Pretty.text path <> ": ") <>)
. Pretty.string
)
( \(v :: Aeson.Value) ->
pure $
if hide
Expand Down Expand Up @@ -309,12 +327,9 @@ run isTest verbosity dir codebase runtime sbRuntime nRuntime ucmVersion baseURL
>>= either
-- invalid command is treated as a failure
( \msg -> do
liftIO $ writeIORef hasErrors True
liftIO (readIORef allowErrors) >>= \case
True -> do
liftIO $ outputUcmResult msg
Cli.returnEarlyWithoutOutput
False -> liftIO . dieWithMsg $ Pretty.toPlain terminalWidth msg
liftIO $ outputUcmResult msg
liftIO $ maybeDieWithMsg msg
Cli.returnEarlyWithoutOutput
)
-- No input received from this line, try again.
(maybe Cli.returnEarlyWithoutOutput $ pure . Right . snd)
Expand All @@ -325,6 +340,7 @@ run isTest verbosity dir codebase runtime sbRuntime nRuntime ucmVersion baseURL
writeIORef isHidden $ hidden infoTags
outputEcho $ pure block
writeIORef allowErrors $ expectingError infoTags
writeIORef expectFailure $ hasBug infoTags
-- Open a ucm block which will contain the output from UCM after processing the `UnisonFileChanged` event.
-- Close the ucm block after processing the UnisonFileChanged event.
atomically . Q.enqueue cmdQueue $ Nothing
Expand All @@ -335,13 +351,15 @@ run isTest verbosity dir codebase runtime sbRuntime nRuntime ucmVersion baseURL
liftIO do
writeIORef isHidden $ hidden infoTags
writeIORef allowErrors $ expectingError infoTags
writeIORef expectFailure $ hasBug infoTags
outputEcho . pure . API infoTags . fold =<< traverse apiRequest apiRequests
Cli.returnEarlyWithoutOutput
Ucm infoTags cmds -> do
liftIO do
writeIORef currentTags $ pure infoTags
writeIORef isHidden $ hidden infoTags
writeIORef allowErrors $ expectingError infoTags
writeIORef expectFailure $ hasBug infoTags
writeIORef hasErrors False
traverse_ (atomically . Q.enqueue cmdQueue . Just) cmds
atomically . Q.enqueue cmdQueue $ Nothing
Expand Down Expand Up @@ -382,6 +400,7 @@ run isTest verbosity dir codebase runtime sbRuntime nRuntime ucmVersion baseURL
liftIO $ writeIORef currentTags Nothing
liftIO $ writeIORef isHidden Shown
liftIO $ writeIORef allowErrors False
liftIO $ writeIORef expectFailure False
maybe (liftIO finishTranscript) (uncurry processStanza) =<< atomically (Q.tryDequeue inputQueue)

awaitInput :: Cli (Either Event Input)
Expand Down Expand Up @@ -409,22 +428,14 @@ run isTest verbosity dir codebase runtime sbRuntime nRuntime ucmVersion baseURL
print :: Output.Output -> IO ()
print o = do
msg <- notifyUser dir o
errOk <- readIORef allowErrors
outputUcmResult msg
when (Output.isFailure o) $
if errOk
then writeIORef hasErrors True
else dieWithMsg $ Pretty.toPlain terminalWidth msg
when (Output.isFailure o) $ maybeDieWithMsg msg

printNumbered :: Output.NumberedOutput -> IO Output.NumberedArgs
printNumbered o = do
let (msg, numberedArgs) = notifyNumbered o
errOk <- readIORef allowErrors
outputUcmResult msg
when (Output.isNumberedFailure o) $
if errOk
then writeIORef hasErrors True
else dieWithMsg $ Pretty.toPlain terminalWidth msg
when (Output.isNumberedFailure o) $ maybeDieWithMsg msg
pure numberedArgs

-- Looks at the current stanza and decides if it is contained in the
Expand All @@ -447,13 +458,21 @@ run isTest verbosity dir codebase runtime sbRuntime nRuntime ucmVersion baseURL
dieUnexpectedSuccess :: IO ()
dieUnexpectedSuccess = do
errOk <- readIORef allowErrors
expectBug <- readIORef expectFailure
hasErr <- readIORef hasErrors
when (errOk && not hasErr) $ do
appendFailingStanza
transcriptFailure
out
"The transcript was expecting an error in the stanza above, but did not encounter one."
Nothing
case (errOk, expectBug, hasErr) of
(True, False, False) -> do
appendFailingStanza
transcriptFailure
out
"The transcript was expecting an error in the stanza above, but did not encounter one."
Nothing
(False, True, False) -> do
fixedBug
out
"The stanza above with `:bug` is now passing! You can remove `:bug` and close any appropriate Github \
\issues."
(_, _, _) -> pure ()

authenticatedHTTPClient <- AuthN.newAuthenticatedHTTPClient tokenProvider ucmVersion

Expand Down Expand Up @@ -508,6 +527,22 @@ transcriptFailure out heading mbody = do
<> foldr ((:) . CMarkCodeBlock Nothing "") [] mbody
)

fixedBug :: IORef (Seq Stanza) -> Text -> IO b
fixedBug out body = do
texts <- readIORef out
-- `CMark.commonmarkToNode` returns a @DOCUMENT@, which won’t be rendered inside another document, so we strip the
-- outer `CMark.Node`.
let CMark.Node _ _DOCUMENT bodyNodes = CMark.commonmarkToNode [CMark.optNormalize] body
UnliftIO.throwIO . RunFailure $
texts
<> Seq.fromList
( Left
<$> [ CMark.Node Nothing CMark.PARAGRAPH [CMark.Node Nothing (CMark.TEXT "🎉") []],
CMark.Node Nothing (CMark.HEADING 2) [CMark.Node Nothing (CMark.TEXT "You fixed a bug!") []]
]
<> bodyNodes
)

data Error
= ParseError (P.ParseErrorBundle Text Void)
| RunFailure (Seq Stanza)
Expand Down
2 changes: 1 addition & 1 deletion unison-src/builtin-tests/interpreter-tests.output.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ If you want to add or update tests, you can create a branch of that project, and

Before merging the PR on Github, we'll merge your branch on Share and restore `runtime_tests_version` to /main or maybe a release.

``` ucm :hide:error
``` ucm :hide :error
scratch/main> this is a hack to trigger an error, in order to swallow any error on the next line.
scratch/main> we delete the project to avoid any merge conflicts or complaints from ucm.
Expand Down
30 changes: 20 additions & 10 deletions unison-src/builtin-tests/jit-tests.output.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ If you want to add or update tests, you can create a branch of that project, and

Before merging the PR on Github, we'll merge your branch on Share and restore `runtime_tests_version` to /main or maybe a release.

``` ucm :hide :error
scratch/main> this is a hack to trigger an error, in order to swallow any error on the next line.
scratch/main> we delete the project to avoid any merge conflicts or complaints from ucm.
scratch/main> delete.project runtime-tests
```

``` ucm :hide
scratch/main> clone @unison/runtime-tests/releases/0.0.1 runtime-tests/selected
```

``` ucm
runtime-tests/selected> run.native tests
Expand All @@ -12,8 +24,8 @@ runtime-tests/selected> run.native tests
runtime-tests/selected> run.native tests.jit.only
()
```

Per Dan:
It's testing a flaw in how we were sending code from a scratch file to the native runtime, when that happened multiple times.
Related to the verifiable refs and recursive functions.
Expand All @@ -27,19 +39,18 @@ foo = do
go 1000
```

``` ucm
``` ucm :added-by-ucm
Loading changes detected in scratch.u.
I found and typechecked these definitions in scratch.u. If you
do an `add` or `update`, here's how your codebase would
change:
⍟ These new definitions are ok to `add`:
foo : '{Exception} ()
```

``` ucm
scratch/main> run.native foo
Expand All @@ -48,20 +59,19 @@ scratch/main> run.native foo
scratch/main> run.native foo
()
```

This can also only be tested by separately running this test, because
it is exercising the protocol that ucm uses to talk to the jit during
an exception.

``` ucm
``` ucm :error
runtime-tests/selected> run.native testBug
💔💥
I've encountered a call to builtin.bug with the following
value:
"testing"
"testing"
```
Loading

0 comments on commit b311a29

Please sign in to comment.