Skip to content

Conversation

@Foxinio
Copy link
Collaborator

@Foxinio Foxinio commented Oct 23, 2025

In this PR I propose module to write structured tests.
(This implementation is inspired by one done by @wojpok, and included in #256)

Tests can be written like so:

let suite = createTestSuite "Prelude/*"

let _ = suite.addTest "`chr` calls on error" (fn () =>
  assertCallsOnError (fn _ => chr 300))

let _ = suite.addTest "`chr` does not call on error" (fn () =>
  assertDoesNotCallOnError (fn _ => chr 127))

# Clearly not correct implementation
let mkStr _ _ = ""

let _ = suite.addTest "`mkStr` count = 0" (fn _ =>
  assertEq (mkStr "abc" 0) "")

let _ = suite.addTest "`mkStr` count < 0" (fn _ =>
    assertEq (mkStr "abc" (-1)) "";
    assertEq (mkStr "abc" (-10)) "")
  
let _ = suite.addTest "`mkStr` count > 0" (fn _ =>
    assertEq (mkStr "abc" 1) "abc";
    assertEq (mkStr "abc" 2) "abcabc";
    assertEq (mkStr "abc" 3) "abcabcabc";
    assertEq (mkStr "abc" 5) "abcabcabcabcabc")

Tests are run whenever file containing tests is included, in one passed to dbl.
This may seem undesirable, but will be addressed with introduction of attributes.

@wojpok
Copy link
Collaborator

wojpok commented Oct 28, 2025

I highly suggest the following style of writing test suites:

let _ =
  let addTest = craeteTestSuite "mySuite" >.addTest in
    addTest "test1" (fn .....);
    addTest "test"2 (fn .....)

That way it will be much easier to introduce @{test ...} attributes

@Foxinio
Copy link
Collaborator Author

Foxinio commented Oct 28, 2025

I highly suggest the following style of writing test suites:

let _ =
  let addTest = craeteTestSuite "mySuite" >.addTest in
    addTest "test1" (fn .....);
    addTest "test"2 (fn .....)

That way it will be much easier to introduce @{test ...} attributes

This would work, if suites were flat structures. But since we made them tree-like, allowing suites to contain other suites, this will not work.

@wojpok
Copy link
Collaborator

wojpok commented Oct 30, 2025

I still consider nested test suites to be a bloat. We dont have fixtures, so there really is not need to complicate this mechanism. Also what difference does make currentSuite.addSuite "nested suite" to directly creating new suite createTestSuite "nested suite"? They still execute in sequence.

@Foxinio
Copy link
Collaborator Author

Foxinio commented Oct 30, 2025

I prefer this solution because of its scalability. While current implementation doesn't really make use of these structures being nested, this module is rich in avenues for extension. So:

  1. User interface isn't significantly impacted by this being nested structures.
  2. Implementation would barely change to go back to flat structures, and as person implementing it I can say, previously it was more complicated.
  3. There are multiple ways this can be extended like:
  • Adding suite prefixes, so you can later filter tests by them.
  • Adding tags to tests and suites, and make tests inherit those tags.
  1. While it is possible to achieve grouping of tests, when using flat structure, it would be cumbersome and you would have to invent a convention and then keep to it without being automatically checked if you really follow this convention. Ultimately this is not necessary when you could have grouping mechanism built into framework at no real cost.

@Foxinio
Copy link
Collaborator Author

Foxinio commented Oct 30, 2025

I also no see that I misunderstood your earlier comment.
I have no problem with introducing your convention, and it may even look nicer.
But I have to emphasize, it doesn't give us any computational benefits.

Copy link
Collaborator

@wojpok wojpok left a comment

Choose a reason for hiding this comment

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

From the functional standpoint, the code is fine. I don't like some of the design decisions. I don't get the idea of global tests and recursive suites. There should be just regular suites and nothing more. Let's discuss this with rest of the team during the next meeting.

, errorType : ErrorType
}

{# TODO : Add tags to tests and suites #}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't. Tags will handled using attributes as already agreed upon

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't believe we did. From what i remember we discussed what attributes should be, but at the time we didn't consider how this would change once there is a proper testing module in place, and we considered that there could be changes.
As to why i think this would be advantageous, firstly having it expressed inside language gives use much more flexibility, secondly it allows for having a far more fine-grained tags.
I can imagine having a test file with

let tags = ...

defined at the top, and being able to pass just this named parameter, instead of having to write it out every time.

However this seems to be another thing to be discussed at the meeting.

lib/Test.fram Outdated
, body : Unit ->[IO] List ErrorInfo
}

data rec TestSuiteInternal =
Copy link
Collaborator

Choose a reason for hiding this comment

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

Neither TestSuiteInternal nor TestSuite are public. User can't interact with them directly, hence why the redundancy?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The second one is abstr, as it should be usable outside the module.
TestSuiteInternal shouldn't be visible outside this module and so the handle is returned.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Abstr makes type visible outside module. User can't interact with this structure unless public function/method is provided, just like you already did. You can move all data from TestSuiteInternal to abstract TestSuite and user will have no idea that there are any additional data. The most delicate part of this structure are references that are in fact already shared. There is no immediate benefit to this structure.

lib/Test.fram Outdated
method printTestSuccess (TestCase { name, line, file }) =
printStr "[ \{ Color.setFgColor Color.Green | strFmt }OK";
printStr "\{Color.resetAll | strFmt} ]";
printStrLn " \{ name | strFmt} from \{ file | strFmt }:\{ line | strFmt }"
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think there is a need to include test definition location. It looks ugly honestly

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

To me it looks fine, i guess something else to vote on :D.

##}
data TestArbiter E =
{ abort : {X} -> ErrorInfo ->[E] X
, fail : ErrorInfo ->[E] Unit
Copy link
Collaborator

Choose a reason for hiding this comment

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

fail is confusing. It does not indicate that contrary to abort the test is resumed upon calling fail.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I couldn't find a better name for saying that if condition isn't met, the test is resumed but is considered failed.
Also in my opinion the type says it very well, fail always returns unit, contrary to abort which never returns.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Fair enough. Consider adding a comment that explains the difference. That will do for me

lib/Test.fram Outdated
let tests = ioMut.ref [] in
let suite' = TestSuiteInternal { name, file, line, tests, suites } in
suite.addSuite suite';
TestSuite { suites, tests }
Copy link
Collaborator

Choose a reason for hiding this comment

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

Reference to recursive suites and test list is shared. Why are you using different 2 objects here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As answered above, this is the place where this is used.

Copy link
Collaborator Author

@Foxinio Foxinio left a comment

Choose a reason for hiding this comment

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

Addressed most of the comments, all that's left is to conform this module to what we agree on in the meeting.

lib/Test.fram Outdated
, body : Unit ->[IO] List ErrorInfo
}

data rec TestSuiteInternal =
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The second one is abstr, as it should be usable outside the module.
TestSuiteInternal shouldn't be visible outside this module and so the handle is returned.

lib/Test.fram Outdated
let tests = ioMut.ref [] in
let suite' = TestSuiteInternal { name, file, line, tests, suites } in
suite.addSuite suite';
TestSuite { suites, tests }
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As answered above, this is the place where this is used.

lib/Test.fram Outdated
method printTestSuccess (TestCase { name, line, file }) =
printStr "[ \{ Color.setFgColor Color.Green | strFmt }OK";
printStr "\{Color.resetAll | strFmt} ]";
printStrLn " \{ name | strFmt} from \{ file | strFmt }:\{ line | strFmt }"
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

To me it looks fine, i guess something else to vote on :D.

, errorType : ErrorType
}

{# TODO : Add tags to tests and suites #}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't believe we did. From what i remember we discussed what attributes should be, but at the time we didn't consider how this would change once there is a proper testing module in place, and we considered that there could be changes.
As to why i think this would be advantageous, firstly having it expressed inside language gives use much more flexibility, secondly it allows for having a far more fine-grained tags.
I can imagine having a test file with

let tags = ...

defined at the top, and being able to pass just this named parameter, instead of having to write it out every time.

However this seems to be another thing to be discussed at the meeting.

lib/Test.fram Outdated
Comment on lines 30 to 41
data rec TestSuiteInternal =
{ name : String
, line : Int
, file : String
, suites : Ref IO (List TestSuiteInternal)
, tests : Ref IO (List TestCase)
}

abstr data TestSuite =
{ suites : Ref IO (List TestSuiteInternal)
, tests : Ref IO (List TestCase)
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is meant to be two part. One is the internal representation, the other is a public facing handle to access test module interface. Probably this could be reduced to one abstract type, and the semantics should be preserved. But i don't see any harm in having both, and separating them based on the use result's in much clearer code.

@wojpok
Copy link
Collaborator

wojpok commented Nov 3, 2025

I have made up my mind. I am fine with this code as is. It would be nice to merge TestSuiteInternal and TestSuite to single structure, but I won't enforce that. Only issue at hand is lack of any mechanism to toggle test excecution. If you provide a suitable way of controlling it I will approve this PR

@Foxinio
Copy link
Collaborator Author

Foxinio commented Nov 4, 2025

I agree, it has to be done, and so I left a TODO comment. However I think it's best to merge it in this state and improve in a later PR, as this change would require a significant amount of code to be written.

In my opinion, for now, we can live with all included tests being run.

@wojpok
Copy link
Collaborator

wojpok commented Nov 4, 2025

Not necessarily. What about a global flag that can be read using extern and would be controlled via designated flag in command line? A neat temporary solution that might be substituted in the future easily

Copy link
Collaborator

@wojpok wojpok left a comment

Choose a reason for hiding this comment

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

Code is okay, all issues are resolved. I am not a fan of the command line output, but I don't really want to slow down the development. We need proper testing environment sooner than later

@Foxinio
Copy link
Collaborator Author

Foxinio commented Dec 15, 2025

Work developed in this PR is to be continued in #289.

@Foxinio Foxinio closed this Dec 15, 2025
@Foxinio Foxinio deleted the testing branch December 15, 2025 11:50
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.

2 participants