Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions src/Elm/Kernel/Test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*

import Elm.Kernel.Utils exposing (Tuple0)
import Elm.Kernel.Utils exposing (Tuple0, Tuple2)
import File exposing (FileNotFound, GeneralFileError, IsDirectory, PathEscapesDirectory)
import Result exposing (Err, Ok)

Expand All @@ -20,6 +20,8 @@ function _Test_runThunk(thunk)

const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const crypto = require('node:crypto');

function _Test_readFile(filePath)
{
Expand All @@ -38,7 +40,7 @@ function _Test_readFile(filePath)
}

try {
return __Result_Ok(fs.readFileSync(fullPath, { encoding: 'utf8' }));
return __Result_Ok(__Utils_Tuple2(fullPath, fs.readFileSync(fullPath, { encoding: 'utf8' })));
}
catch (err)
{
Expand All @@ -51,18 +53,17 @@ function _Test_readFile(filePath)
}
}

var _Test_writeFile = F2(function(filePath, contents)
function WriteFile(root, filePath, contents)
{
// Test for this early as `resolve` will strip training slashes
if (filePath.slice(-1) == path.sep) {
return __Result_Err(__File_IsDirectory);
}

// Protect against writing files above the "tests" directory
const testsPath = path.resolve("tests");
const fullPath = path.resolve(testsPath, filePath);
// Protect against writing files above the root directory
const fullPath = path.resolve(root, filePath);

if (!fullPath.startsWith(testsPath))
if (!fullPath.startsWith(root))
{
return __Result_Err(__File_PathEscapesDirectory);
}
Expand All @@ -74,10 +75,27 @@ var _Test_writeFile = F2(function(filePath, contents)

try {
fs.writeFileSync(fullPath, contents);
return __Result_Ok(__Utils_Tuple0);
return __Result_Ok(fullPath);
}
catch (err)
{
return __Result_Err(__File_GeneralFileError(err.toString()));
}
}

var _Test_writeFile = F2(function(filePath, contents)
{
return WriteFile(path.resolve("tests"), filePath, contents);
})

var tempDir = null;
var _Test_writeTempFile = F2(function(filePath, contents)
{
if (tempDir === null)
{
tempDir = os.tmpdir() + "/" + crypto.randomUUID();
fs.mkdirSync(tempDir);
}

return WriteFile(tempDir, filePath, contents);
})
52 changes: 39 additions & 13 deletions src/Expect.elm
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ module Expect exposing
, lessThan, atMost, greaterThan, atLeast
, FloatingPointTolerance(..), within, notWithin
, ok, err, equalLists, equalDicts, equalSets
, pass, fail, onFail, equalToFile
, equalToFile
, pass, fail, onFail
)

{-| A library to create `Expectation`s, which describe a claim to be tested.
Expand Down Expand Up @@ -42,10 +43,12 @@ or both. For an in-depth look, see our [Guide to Floating Point Comparison](#gui

@docs ok, err, equalLists, equalDicts, equalSets


## Golden Files

@docs equalToFile


## Customizing

These functions will let you build your own expectations.
Expand Down Expand Up @@ -106,8 +109,8 @@ Another example is comparing values that are on either side of zero. `0.0001` is
-}

import Dict exposing (Dict)
import Set exposing (Set)
import File
import Set exposing (Set)
import Test.Distribution
import Test.Expectation
import Test.Internal as Internal
Expand Down Expand Up @@ -580,7 +583,7 @@ equalSets expected actual =
reportCollectionFailure "Expect.equalSets" expected actual missingKeys extraKeys


{-| Tests the a String is equal to the contents of the file stored at the file path.
{-| Tests the a String is equal to the contents of the file stored at the file path.

If the file does not exist, it will be created and this test will pass.

Expand All @@ -591,30 +594,53 @@ All file paths are scoped to be within the "tests/" directory.
-}
equalToFile : String -> String -> Expectation
equalToFile filePath actual =
case File.readFile filePath of
case File.readFile filePath of
Err File.FileNotFound ->
case File.writeFile filePath actual of
Err (File.GeneralFileError fileError) ->
case File.writeFile filePath actual of
Err (File.GeneralFileError fileError) ->
Test.Expectation.fail { description = "Expect.equalToFile encountered a general file error: " ++ fileError, reason = Custom }

-- This case should be impossible non general file errors should have been surfaced in the call to `readFile` above.
Err _ ->
Test.Expectation.fail { description = "Expect.equalToFile encountered an unexpected error", reason = Custom }
Err _ ->
Test.Expectation.fail { description = "Expect.equalToFile encountered an unexpected error", reason = Custom }

Ok _ ->
pass

Err File.IsDirectory ->
Err File.IsDirectory ->
Test.Expectation.fail { description = "Expect.equalToFile was given a directory instead of a file", reason = Custom }

Err File.PathEscapesDirectory ->
Err File.PathEscapesDirectory ->
Test.Expectation.fail { description = "Expect.equalToFile was given a path that would escape the tests/ directory", reason = Custom }

Err (File.GeneralFileError fileError) ->
Err (File.GeneralFileError fileError) ->
Test.Expectation.fail { description = "Expect.equalToFile encountered a general file error: " ++ fileError, reason = Custom }

Ok contents ->
equateWith ("equalToFile \'" ++ filePath ++ "\'") (==) contents actual
Ok ( existingAbsolutePath, contents ) ->
if actual == contents then
pass

else
case File.writeTempFile filePath actual of
Ok newAbsolutePath ->
let
message =
[ "The contents of \"" ++ filePath ++ "\" changed!"
, "To compare run: git diff --no-index " ++ existingAbsolutePath ++ " " ++ newAbsolutePath
]

messageWithVisualDiff =
if String.endsWith ".html" filePath then
message ++ [ "To visually compare run: open file://" ++ existingAbsolutePath ++ " file://" ++ newAbsolutePath ]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not sure how hard we're trying to make this cross-platform but open is MacOS specific.
On Linux I think it's xdg-open. I don't know the command on Windows, but I think the path would have backslashes that would have to be converted to forward slashes for the URL.
I think it's best not to worry about this for now though! If this ever gets upstreamed it would need to be tested and patched by someone on those platforms.


else
message
in
Test.Expectation.fail { description = String.join "\n\n" messageWithVisualDiff, reason = Custom }

_ ->
Test.Expectation.fail { description = "Expect.equalToFile encountered an unexpected error", reason = Custom }


{-| Always passes.

Expand Down
40 changes: 35 additions & 5 deletions src/File.elm
Original file line number Diff line number Diff line change
@@ -1,15 +1,45 @@
module File exposing (readFile, writeFile, FileError(..))
module File exposing (AbsolutePath, FileError(..), RelativePath, readFile, writeFile, writeTempFile)

import Elm.Kernel.Test


type FileError
= FileNotFound
| IsDirectory
| PathEscapesDirectory
| GeneralFileError String

readFile : String -> Result FileError String
readFile = Elm.Kernel.Test.readFile

writeFile : String -> String -> Result FileError ()
writeFile = Elm.Kernel.Test.writeFile
type alias RelativePath =
String


type alias AbsolutePath =
String


{-| Read the contents of the filePath relative to "tests/"
-}
readFile : RelativePath -> Result FileError ( AbsolutePath, String )
readFile =
Elm.Kernel.Test.readFile


{-| Write the contents of the second argument to the file path in the first argument relative to "tests/"

Returns the absolute file path if successful.

-}
writeFile : RelativePath -> String -> Result FileError AbsolutePath
writeFile =
Elm.Kernel.Test.writeFile


{-| Write the contents of the second argument to the file path in the first argument relative to a temp directory

Returns the absolute file path if successful.

-}
writeTempFile : RelativePath -> String -> Result FileError AbsolutePath
writeTempFile =
Elm.Kernel.Test.writeTempFile