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

Add support for “known failures” to transcripts #5394

Merged
merged 9 commits into from
Dec 11, 2024
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
46 changes: 30 additions & 16 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")
<|> (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
73 changes: 44 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,23 @@ 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) ->
transcriptFailure
out
( Text.pack . Pretty.toPlain terminalWidth $
"The stanza above previously had an incorrect successful result, but now fails with"
aryairani marked this conversation as resolved.
Show resolved Hide resolved
<> "\n"
<> Pretty.border 2 msg
<> "\n"
<> "if this is the expected result, remove `:bug`, otherwise remove `:error`."
aryairani marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

@aryairani aryairani Dec 10, 2024

Choose a reason for hiding this comment

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

I think having four terms is too many, I wonder if we can do it with no more than two.

Yes to "is the error the error we're actually expecting?" that's where I was heading with the "open a new ticket if needed".

I'd be satisfied with this, wdyt:

Suggested change
"The stanza above previously had an incorrect successful result, but now fails with"
<> "\n"
<> Pretty.border 2 msg
<> "\n"
<> "if this is the expected result, remove `:bug`, otherwise remove `:error`."
"The stanza above marked with `:error :bug` is now failing with"
<> "\n"
<> Pretty.border 2 msg
<> "\n"
<> "so you can remove `:bug`, close any appropriate Github issues by using the commit message or PR description, and/or open a new issue if the error message is still not correct."

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, yeah – mentioning closing the related issue is important! I’ll do the same on the the other :bug case. And yeah, this message looks good to me.

I think there’s still four terms, but different axes, and ones that match our tags better: passing/failing (:error) and expected/unexpected (:bug). So yeah, I like that direction.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I changed the phrasing a bit again, so see what you think in the latest commits.

I also realized that the only tests added were for actual outstanding bugs, so I added an additional transcript that will continue testing the “positive” cases after those outstanding bugs are fixed, as well as two error tests for fixed bugs (which show the messages we’ve been fiddling with).

)
Nothing
(_, _) -> pure ()

apiRequest :: APIRequest -> IO [APIRequest]
apiRequest req = do
Expand All @@ -220,9 +232,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 +325,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 +338,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 +349,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 +398,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 +426,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 +456,19 @@ 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
appendFailingStanza
transcriptFailure out "The stanza above is now passing! Please remove `:bug` from it." Nothing
(_, _, _) -> pure ()

authenticatedHTTPClient <- AuthN.newAuthenticatedHTTPClient tokenProvider ucmVersion

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"
```
20 changes: 20 additions & 0 deletions unison-src/transcripts-using-base/fix5178.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
``` unison
foo = {{
@source{Stream.emit}
}}
```

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

Viewing `foo` via `scratch/main> ui` shows the correct source, but `display foo` gives us an error message (but not an error – this is incorrectly considered a successful result)

I think there are two separate issues here:

1. this message should be considered an error, not success; and
2. this should actually work like `ui` and give us the source of the ability member, not complain about there being no such term in the codebase.

``` ucm :error :bug
scratch/main> display foo
```
Loading
Loading