#| Utilities for interacting with idris and associated tooling unit module IUtils; need IUtils::IDEMode; use Terminal::ANSIColor; use IUtils::Regexes; use IUtils::Compiler; use paths; #| Structure representing a test class Test { #| The name of the test has Str:D $.name is required; #| The expression name of the test has Str:D $.expr is required; #| The output type of the test has ExprOutput:D $.output-type is required; #| Run this test, and return true if it failed, false if it passed method run(IO::Path:D $source, Int:D $indent-level? = 4 --> Bool) { CATCH { when ExpressionError { say "{colored '+', 'red'} $.name: {colored 'FAIL', 'red bold'}" .indent($indent-level); say "{colored('exit code', 'red')}: {$_.exit-code}" .indent($indent-level + 2); if $_.out.trim { say colored('stdout:', 'underline') .indent($indent-level + 2); say $_.out.trim.lines.map(*.indent($indent-level + 2)) .join("\n"); } if $_.err.trim { say colored('stderr:', 'underline') .indent($indent-level + 2); say $_.err.trim.lines.map(*.indent($indent-level + 2)) .join("\n"); } return True; } } idris-exec $.expr, $source.relative, $.output-type; # The exception handler graps flow if the test failed, here the test passed my $output = "{colored '+', 'green'} $.name: {colored 'pass', 'green'}"; say $output.indent($indent-level); return False; } } #| Structure representing the tests in a module class ModuleTests { #| The name of this module has Str:D $.name is required; #| The source file of this module has IO::Path:D $.source is required; #| A list of the associated tests this module has has Test:D @.tests is required; } #| Structure representing all of the runables assocated with a project class PackageRunables { #| The ipkg for this project has IO::Path:D $.ipkg is required; # TODO: Add benchmarks #| A map from the name of the module to a list of tests has ModuleTests:D %.tests is required; } #| Structure representing the root of what idris considers a package directory, #| with the associated ipkg and source files. These can and will overlap within #| the same directory. class PackageInfo { has IO::Path:D $.ipkg is required; has IO::Path:D $.root is required; has IO::Path:D @.sources is required; method runnables { # Locate the tests my %tests = Hash.new; for @.sources -> $source { my $contents = $source.slurp; if $contents ~~ &module-name { my $module-name = $.Str; my @tests; for $contents.match(&flagged-expression, :g) -> $match { my $output-type = do given $match { when * eq '()' {succeed Unit}; when * eq 'Bool' {succeed Boolean}; when * eq 'Either' {succeed Either}; }; my $test = Test.new(name => $match.Str.trim, expr => $match.Str, output-type => $output-type); @tests.push($test); } if @tests.elems > 0 { %tests{$module-name} = ModuleTests.new(name => $module-name, source => $source, tests => @tests); } } } # Build and return the runnables PackageRunables.new(ipkg => self.ipkg, tests => %tests) } } #| Scan a particular ipkg for its associated sources sub scan-ipkg(IO::Path:D $ipkg --> PackageInfo:D) { my $contents = $ipkg.slurp; my $src-dir = ($contents ~~ / 'sourcedir' \h* '=' \h* '"' $=[<-["]>*] '"' /) // "src"; my IO::Path:D @sources = paths($ipkg.parent.add($src-dir), :file(*.ends-with(".idr"))).map(*.IO); PackageInfo.new(ipkg => $ipkg, root => $ipkg.parent, sources => @sources) } # TODO: Add some parsing of pack.toml to locate test packages and associate them # with their source ipkg #| Scan $*CWD to locate ipkgs and their associated sources sub scan-packages(--> Array[PackageInfo:D]) is export { my PackageInfo:D @ipkgs = paths(:file(*.ends-with(".ipkg"))).map(*.IO.&scan-ipkg); return @ipkgs; }