diff --git a/build.gradle b/build.gradle index abe0b4fd7..7b6a18e75 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,7 @@ repositories { ext { goloCliMain = 'org.eclipse.golo.cli.Main' goloSources = fileTree('src/main/golo').include('**/*.golo') + goloTests = fileTree('src/test/golo').include('**/*.golo') goloDocs = file("$buildDir/docs/golodoc") } @@ -107,6 +108,17 @@ test { // .................................................................................................................. // +task golotest(type: JavaExec, dependsOn: [testClasses]) { + main = goloCliMain + args = ['test', '--files'] + goloTests + classpath = sourceSets.main.runtimeClasspath + inputs.files goloTests + description = 'Run Golo Tests' + group = 'Test' +} + +// .................................................................................................................. // + processResources { filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: [ version: version, @@ -331,4 +343,4 @@ task wrapper(type: Wrapper) { description 'Generates the Gradle wrapper scripts.' } -// .................................................................................................................. // +// .................................................................................................................. // \ No newline at end of file diff --git a/src/main/golo/testing.golo b/src/main/golo/testing.golo new file mode 100644 index 000000000..3f85070e9 --- /dev/null +++ b/src/main/golo/testing.golo @@ -0,0 +1,37 @@ +module gololang.Testing + +import gololang.testing.Runner +import gololang.testing.Suite +import gololang.testing.Test +import gololang.testing.Reporter + +augment gololang.testing.Runner.types.Runner { + + function describe = |this, description, fn| { + let parent = this: currentSuite() + let suite = Suite( + description, + parent, + this: reporter(): onSuiteStarted(), + this: reporter(): onSuiteDone() + ) + parent: add(suite) + this: currentSuite(suite) + fn() + this: currentSuite(parent) + } + + function it = |this, description, fn| { + let parent = this: currentSuite() + parent: add(Test( + description, + fn, + parent, + this: reporter(): onTestStarted(), + this: reporter(): onTestDone() + )) + } + + function beforeEach = |runner, fn| -> runner: currentSuite(): addBeforeEach(fn) + function afterEach = |runner, fn| -> runner: currentSuite(): addAfterEach(fn) +} \ No newline at end of file diff --git a/src/main/golo/testing/console.golo b/src/main/golo/testing/console.golo new file mode 100644 index 000000000..f6bfaba81 --- /dev/null +++ b/src/main/golo/testing/console.golo @@ -0,0 +1,94 @@ +module gololang.testing.presenters.Console + +import gololang.testing.Presenter +import gololang.AnsiCodes + + +function Console = { + let level = DynamicObject(): position(-1): indentSize(2) + + let fwd = -> level: position(level: position() + 1) + let bwd = -> level: position(level: position() - 1) + let spaces = -> (level: position() * level: indentSize()): times({ print(" ")}) + + return Presenter() + :onTestStarted(|test| { + fwd() + spaces() + _onTestStarted(test) + }) + :onTestDone(|test| { + spaces() + _onTestDone(test) + bwd() + }) + :onSuiteStarted(|suite| { + fwd() + spaces() + _onSuiteStarted(suite) + }) + :onSuiteDone(|suite| { + spaces() + _onSuiteDone(suite) + bwd() + }) + :onGlobalStarted(|runner| { + _onGlobalStarted(runner) + }) + :onGlobalDone(|runner| { + _onGlobalDone(runner) + }) +} + +local function success = |msg| { + fg_green() + println(msg) + reset() +} + +local function error = |msg| { + fg_red() + println(msg) + reset() +} + +local function warning = |msg| { + fg_yellow() + println(msg) + reset() +} + +local function info = |msg| { + fg_blue() + println(msg) + reset() +} + +local function _onTestStarted = |test| { +# info(test: description()) +} + +local function _onTestDone = |test| { + if (test: failed()) { + error(test: description()) + } else { + success(test: description()) + } +} + +local function _onSuiteStarted = |suite| { +# success(suite: description()) + println(suite: description()) +} + +local function _onSuiteDone = |suite| { +# error(suite: description()) +} + +local function _onGlobalStarted = |runner| { +# println("Global started...") +} + +local function _onGlobalDone = |runner| { + info("Total " + runner: currentSuite(): report(): total() + " tests ran. Failures " + runner: currentSuite(): report(): failures()) +} \ No newline at end of file diff --git a/src/main/golo/testing/presenter.golo b/src/main/golo/testing/presenter.golo new file mode 100644 index 000000000..a338a8806 --- /dev/null +++ b/src/main/golo/testing/presenter.golo @@ -0,0 +1,11 @@ +module gololang.testing.Presenter + +struct Presenter = { + onGlobalStarted, + onGlobalDone, + onSuiteStarted, + onSuiteDone, + onTestStarted, + onTestDone +} + diff --git a/src/main/golo/testing/reporter.golo b/src/main/golo/testing/reporter.golo new file mode 100644 index 000000000..9ee0c8ae9 --- /dev/null +++ b/src/main/golo/testing/reporter.golo @@ -0,0 +1,24 @@ +module gololang.testing.Reporter + +struct Reporter = { + presenters +} + +function Reporter = -> gololang.testing.Reporter.types.Reporter(list[]) + +augment gololang.testing.Reporter.types.Reporter { + + function addPresenter = |this, presenter| { this: presenters(): add(presenter) } + + function onTestStarted = |this| -> |test| -> this: presenters(): each(|p| -> p: onTestStarted()(test)) + + function onTestDone = |this| -> |test| -> this: presenters(): each(|p| -> p: onTestDone()(test)) + + function onSuiteStarted = |this| -> |suite| -> this: presenters(): each(|p| -> p: onSuiteStarted()(suite)) + + function onSuiteDone = |this| -> |suite| -> this: presenters(): each(|p| -> p: onSuiteDone()(suite)) + + function onGlobalStarted = |this, runner| -> this: presenters(): each(|p| -> p: onGlobalStarted()(runner)) + + function onGlobalDone = |this, runner| -> this: presenters(): each(|p| -> p: onGlobalDone()(runner)) +} \ No newline at end of file diff --git a/src/main/golo/testing/runner.golo b/src/main/golo/testing/runner.golo new file mode 100644 index 000000000..59f64026a --- /dev/null +++ b/src/main/golo/testing/runner.golo @@ -0,0 +1,31 @@ +module gololang.testing.Runner + +import gololang.testing.Utils + +import gololang.testing.Suite +import gololang.testing.Test +import gololang.testing.Reporter +import gololang.testing.presenters.Console + +struct Runner = { + currentSuite, + reporter +} + +function build = { + let reporter = Reporter() + let top_level_suite = Suite("TOP_LEVEL_SUITE", null, NO_OP_1(), NO_OP_1()) + let runner = Runner(top_level_suite, reporter) + runner: addPresenter(Console()) + return runner +} + +augment gololang.testing.Runner.types.Runner { + function run = |this| { + this: reporter(): onGlobalStarted(this) + this: currentSuite(): run() + this: reporter(): onGlobalDone(this) + } + + function addPresenter = |this, presenter| -> this: reporter(): addPresenter(presenter) +} \ No newline at end of file diff --git a/src/main/golo/testing/suite.golo b/src/main/golo/testing/suite.golo new file mode 100644 index 000000000..3d2780c2f --- /dev/null +++ b/src/main/golo/testing/suite.golo @@ -0,0 +1,73 @@ +module gololang.testing.Suite + +import gololang.testing.Test + +struct Suite = { + description, + parent, + children, + befores, + afters, + onStart, + onDone, + report +} + +struct SuiteReport = { + total, + failures +} + +function Suite = |description, parent, onStart, onDone| -> gololang.testing.Suite.types.Suite() +:description(description) +:parent(parent) +:children(list[]) +:befores(list[]) +:afters(list[]) +:onStart(onStart) +:onDone(onDone) +:report(SuiteReport(0, 0)) + +augment gololang.testing.Suite.types.SuiteReport { + function addExcutedTest = |this, child| { + if child oftype gololang.testing.Suite.types.Suite.class { + this: total( this: total() + child: report(): total() ) + } else { + this: total( this: total() + 1) + } + } + function addFailures = |this, child| { + if child oftype gololang.testing.Suite.types.Suite.class { + this: failures( this: failures() + child: report(): failures() ) + } else { + this: failures( this: failures() + 1) + } + } +} + +augment gololang.testing.Suite.types.Suite { + + function run = |this| { + this: onStart()(this) + this: children(): each(|child| { + this: befores(): each(|before| -> before()) + child: run() + this: report(): addExcutedTest(child) + if child: failed() { + this: report(): addFailures(child) + } + this: afters(): each(|after| -> after()) + }) + this: onDone()(this) + } + + function add = |this, it| { + it: parent(this) + this: children(): add(it) + } + + function failed = |this| -> this: report(): failures() isnt 0 + + function addBeforeEach = |this, before| -> this: befores(): add(before) + function addAfterEach = |this, after| -> this: afters(): add(after) +} \ No newline at end of file diff --git a/src/main/golo/testing/test.golo b/src/main/golo/testing/test.golo new file mode 100644 index 000000000..37184b5d6 --- /dev/null +++ b/src/main/golo/testing/test.golo @@ -0,0 +1,34 @@ +module gololang.testing.Test + +struct Test = { + description, + fn, + parent, + onStart, + onDone, + status +} + +function Test = |description, fn, parent, onStart, onDone| -> gololang.testing.Test.types.Test() +:description(description) +:fn(fn) +:parent(parent) +:onStart(onStart) +:onDone(onDone) +:status("not_started") + +local function _failed = -> "failed" + +augment gololang.testing.Test.types.Test { + function run = |this| { + this: onStart()(this) + try { + this: fn()() + } catch (e) { + this: status(_failed()) + } + this: onDone()(this) + } + + function failed = |this| -> this: status() is _failed() +} \ No newline at end of file diff --git a/src/main/golo/testing/utils.golo b/src/main/golo/testing/utils.golo new file mode 100644 index 000000000..ee57d47c0 --- /dev/null +++ b/src/main/golo/testing/utils.golo @@ -0,0 +1,3 @@ +module gololang.testing.Utils + +function NO_OP_1 = -> |x|{} \ No newline at end of file diff --git a/src/main/java/org/eclipse/golo/cli/command/TestCommand.java b/src/main/java/org/eclipse/golo/cli/command/TestCommand.java new file mode 100644 index 000000000..6c7872e10 --- /dev/null +++ b/src/main/java/org/eclipse/golo/cli/command/TestCommand.java @@ -0,0 +1,123 @@ +package org.eclipse.golo.cli.command; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import org.eclipse.golo.cli.command.spi.CliCommand; +import org.eclipse.golo.compiler.GoloClassLoader; +import org.eclipse.golo.compiler.GoloCompilationException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import static java.lang.invoke.MethodHandles.lookup; +import static java.lang.invoke.MethodType.genericMethodType; + +@Parameters(commandNames = {"test"}, commandDescription = "Run golo tests") +public class TestCommand implements CliCommand { + + public static final String TEST_METHOD_NAME = "spec"; + public static final MethodType TEST_METHOD_TYPE = genericMethodType(1); + public static final String BUILD_METHOD_NAME = "build"; + + @Parameter(names = "--files", variableArity = true, description = "Test files (*.golo and directories)", required = true) + List files = new LinkedList<>(); + + @Parameter(names = "--classpath", variableArity = true, description = "Classpath elements (.jar and directories)") + List classpath = new LinkedList<>(); + + private final GoloClassLoader loader; + + public TestCommand() throws Throwable { + URLClassLoader primaryClassLoader = primaryClassLoader(this.classpath); + Thread.currentThread().setContextClassLoader(primaryClassLoader); + this.loader = new GoloClassLoader(primaryClassLoader); + } + + private Object runner() throws Throwable { + Class runnerClass = Class.forName("gololang.testing.Runner"); + //TODO replace the search with the exact signature when the Runner struct will be stabilized + Method m = Arrays.asList(runnerClass.getDeclaredMethods()).stream().filter(method -> method.getName().equals(BUILD_METHOD_NAME)).findFirst().get(); + MethodHandle mh = lookup().unreflect(m); + return mh.invokeExact(); + } + + @Override + public void execute() throws Throwable { + Object runner = runner(); + Consumer loadSpecification = clazz -> loadSpecification(runner, clazz); + files.stream() + .map(Paths::get) + .flatMap(this::treeFiles) + .map(this::pathToClass) + .forEach(loadSpecification); + run(runner); + } + + private Stream treeFiles(Path path) { + return listFiles(path).flatMap(it -> + it.toFile().isDirectory() ? + treeFiles(path) : + Stream.of(it) + ); + } + + private Stream listFiles(Path path) { + if (path.toFile().isDirectory()) { + try { + return Files.list(path).filter(testFile -> testFile.toString().endsWith(".golo")); + } catch (IOException e) { + System.out.println(e.getMessage()); + } + } + return Stream.of(path); + } + + private void run(Object runner) throws Throwable { + Class augmentions = Class.forName("gololang.testing.Runner$gololang$testing$Runner$types$Runner"); + MethodHandle run = lookup().findStatic(augmentions, "run", genericMethodType(1)); + run.invoke(runner); + } + + //TODO refactor with CLICommand file loader + private Class pathToClass(Path filepath) { + File file = filepath.toFile(); + try (FileInputStream in = new FileInputStream(file)) { + return loader.load(file.getName(), in); + } catch (GoloCompilationException e) { + handleCompilationException(e); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + //TODO refactor with CLICommand#callRun method + private void loadSpecification(Object runner, Class klass) { + MethodHandle mh = null; + try { + mh = lookup().findStatic(klass, TEST_METHOD_NAME, TEST_METHOD_TYPE); + } catch (NoSuchMethodException e) { + System.out.println(e.getMessage()); + } catch (IllegalAccessException e) { + System.out.println(e.getMessage()); + } + if (mh != null) { + try { + mh.invoke(runner); + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + } + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/services/org.eclipse.golo.cli.command.spi.CliCommand b/src/main/resources/META-INF/services/org.eclipse.golo.cli.command.spi.CliCommand index 6fed53d16..f6488afac 100644 --- a/src/main/resources/META-INF/services/org.eclipse.golo.cli.command.spi.CliCommand +++ b/src/main/resources/META-INF/services/org.eclipse.golo.cli.command.spi.CliCommand @@ -12,5 +12,6 @@ org.eclipse.golo.cli.command.DiagnoseCommand org.eclipse.golo.cli.command.DocCommand org.eclipse.golo.cli.command.GoloGoloCommand org.eclipse.golo.cli.command.InitCommand +org.eclipse.golo.cli.command.TestCommand org.eclipse.golo.cli.command.RunCommand -org.eclipse.golo.cli.command.VersionCommand \ No newline at end of file +org.eclipse.golo.cli.command.VersionCommand diff --git a/src/test/golo/reporterSpec.golo b/src/test/golo/reporterSpec.golo new file mode 100644 index 000000000..1f89e62fd --- /dev/null +++ b/src/test/golo/reporterSpec.golo @@ -0,0 +1,68 @@ +module gololang.ReporterSpec + +import gololang.testing.Presenter +import gololang.testing.Runner +import gololang.Testing + +function ListPresenter = |eventStore| { + return Presenter() + :onTestStarted(|t| -> eventStore: add(t: description() + " started")) + :onTestDone(|t| -> eventStore: add(t: description() + " done")) + :onSuiteStarted(|s| -> eventStore: add(s: description() + " started")) + :onSuiteDone(|s| -> eventStore: add(s: description() + " done")) + :onGlobalStarted({eventStore: add("Global started")}) + :onGlobalDone({eventStore: add("Global done")}) +} + +function dummyRunner = |eventStore| { + let runner = build() + runner: addPresenter(ListPresenter(eventStore)) + return runner +} + +function spec = |$| { + + $: describe("A reporter", { + + $: it("should trigger events in order", { + let events = list[] + let fn = { events: add("") } + let r = dummyRunner(events) + r: describe("Suite 1", { + r: it("Test A", fn) + r: describe("Suite 2", { + r: it("Test B", fn) + }) + r: it("Test C", fn) + }) + + r: describe("Suite 3", {}) + r: run() + + let expected = list[] + :append("Global started") + :append("Suite 1 started") + :append("Test A started") + :append("!") + :append("Test A done") + :append("Suite 2 started") + :append("Test B started") + :append("") + :append("Test B done") + :append("Suite 2 done") + :append("Test C started") + :append("") + :append("Test C done") + :append("Suite 1 done") + :append("Suite 3 started") + :append("Suite 3 done") + :append("Global done") + + require(events: size() == expected: size(), "should math expected size") + for (var i = 0, i < events: size(), i = i + 1) { + require(events: get(i) == expected: get(i), "expected is " + expected: get(i) + " actual is " + events: get(i)) + } + + }) +}) +} \ No newline at end of file diff --git a/src/test/golo/suiteSpec.golo b/src/test/golo/suiteSpec.golo new file mode 100644 index 000000000..60aa8101d --- /dev/null +++ b/src/test/golo/suiteSpec.golo @@ -0,0 +1,50 @@ +module gololang.SuiteSpec + +import gololang.Testing +import gololang.testing.Runner +import gololang.testing.Suite +import gololang.testing.Test +import gololang.testing.Reporter +import gololang.testing.Utils + + +function dummyTest = |fn| -> gololang.testing.Test.Test("dummyTest", fn, null, NO_OP_1(), NO_OP_1()) +function dummySuite = -> gololang.testing.Suite.Suite("dummySuite", null, NO_OP_1(), NO_OP_1()) + +function spec = |$| { + + $: describe("A suite", { + +# $: beforeEach({ +# println("Before each") +# }) + + $: it("should set a test parent to this when added", { + let test = dummyTest({}) + let suite = dummySuite() + require(suite: children(): isEmpty(), "The suite should be empty") + suite: add(test) + require(suite: children(): size() == 1, "The test should be added") + }) + + $: describe("Another one", { + + $: it("should run beforeEach before every single test", { + let myList = list[] + let before = { myList: add("before") } + let test1 = dummyTest({myList: add("test1")}) + let test2 = dummyTest({myList: add("test2")}) + let suite = dummySuite() + suite: add(test1) + suite: add(test2) + suite: addBeforeEach(before) + suite: run() + require(myList: join("-") == "before-test1-before-test2", "the befores should be before the tests") + }) + + }) +# $: afterEach({ +# println("After each") +# }) + }) +} \ No newline at end of file