Skip to content

Commit

Permalink
Initial commit of XUnit.
Browse files Browse the repository at this point in the history
  • Loading branch information
thegridman committed Jun 15, 2023
1 parent a52d981 commit 5451e65
Show file tree
Hide file tree
Showing 68 changed files with 3,574 additions and 3 deletions.
32 changes: 29 additions & 3 deletions lib_ecstasy/src/main/x/ecstasy/annotations/Test.x
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* there must also exist a constructor on the class with no non-default parameters. Lastly, if the
* `group` is specified as [Omit], then the method is **not** _callable_.
*
* The annotation provides two optional parameters that are used to tailor the unit test
* The annotation provides optional parameters that are used to tailor the unit test
* specification for methods and constructors:
*
* * [group] - this assigns the test to a named group of tests, which allows specific groups of
Expand All @@ -32,15 +32,29 @@
* Other group names can be used; any other names are expected to be treated as normal unit tests
* unless the test runner (such as `xunit`) is configured otherwise.
*
* [priority] - this assigns an ordering to execution of the annotated resource. Best practice is
* that unit tests should be agnostic of ordering, but other annotated resources, such as before
* test or after test methods, test extensions, etc. may require ordering to be specified.
* Priority order is highest first, the reverse of the natural order of an Int.
*
* * [expectedException] - if this is non-Null, it indicates that the unit test must throw the
* specified type of exception, otherwise the test will be considered a failure. This option is
* useful for a test that is expected to always fail with an exception.
*
* The parameters are ignored when the annotation is used on classes and properties. Any usage other
* than that specified above may result in a compile-time and/or load/link-time error.
*/
mixin Test(String group = Unit, Type<Exception>? expectedException = Null)
into Class | Property | Method | Function {
mixin Test(String group = Unit, Int priority = 0, Type<Exception>? expectedException = Null)
implements Orderable
into Module | Package | Class | Property | Method | Function {

/**
* @return `True` if this `Test` is in the `Omit` group.
*/
Boolean omitted() {
return group == Omit;
}

/**
* Use this [group] value to indicate a normal unit test. This is the default test group name.
*/
Expand All @@ -56,4 +70,16 @@ mixin Test(String group = Unit, Type<Exception>? expectedException = Null)
* Use this [group] value to indicate that the method must **not** be treated as a unit test.
*/
static String Omit = "omit";

/**
* The valid targets for a Test annotation.
*/
typedef Module | Package | Class | Property | Method | Function as TestTarget;

// ----- Orderable -----------------------------------------------------------------------------

static <CompileType extends Test> Ordered compare(CompileType value1, CompileType value2) {
// the reverse of the natural order of an Int
return value2.priority <=> value1.priority;
}
}
47 changes: 47 additions & 0 deletions lib_xunit/src/main/x/xunit.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* The XUnit test framework module.
*/
module xunit.xtclang.org {
package collections import collections.xtclang.org;

/**
* A mixin that marks a module as a test suite.
*
* ToDo: we should eventually be able to remove this when there is a proper "xtest"
* Ecstasy test executable that will execute tests for a given module in the same
* way that "xec" executes a module.
*/
mixin Suite
into Module {
/**
* Discover and execute all the test fixtures in the `Module`.
*/
void test() {
}
}

/**
* A `Method` or a `Function`.
*/
typedef Method<Object, Tuple<>, Tuple<>> | Function<<>, Tuple<>> as TestMethodOrFunction;


/**
* An identifier of a test fixture.
*
* @param uniqueId the `UniqueId` for the test fixture in the test hierarchy
* @param displayName the human readable display name to use for the test fixture
*/
const TestIdentifier(UniqueId uniqueId, String displayName);

/**
* A function that performs a predicate check on a test fixture.
*/
typedef function Boolean (Object) as FixturePredicate;

static FixturePredicate MethodFixturePredicate = o -> o.is(Method);

static FixturePredicate ClassFixturePredicate = o -> o.is(Class);

static FixturePredicate PackageFixturePredicate = o -> o.is(Package);
}
65 changes: 65 additions & 0 deletions lib_xunit/src/main/x/xunit/DiscoveryConfiguration.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import xunit.selectors.ModuleSelector;

/**
* The configuration used to determine how test fixtures and tests are discovered
* for a specific test execution.
*
* A test discovery configuration may be to discover just a single test method, or
* discover all tests in a class, package or module, or all tests of a specific
* test group, or a combination of any of these.
*/
const DiscoveryConfiguration(DisplayNameGenerator displayNameGenerator = DisplayNameGenerator.Default) {
/**
* Return this `DiscoveryConfiguration` as a `Builder`.
*/
Builder asBuilder() {
return new Builder(this);
}

/**
* Create a default `DiscoveryConfiguration`.
*/
static DiscoveryConfiguration create() {
return builder().build();
}

/**
* Create a `DiscoveryConfiguration` builder to discover test fixtures using the specified `Selector`s.
*/
static Builder builder() {
return new Builder();
}

/**
* A `DiscoveryConfiguration` builder.
*
* @param selectors the `Selector`s to use to discover test fixtures
*/
static class Builder {
/**
* Create a `Builder`.
*/
construct() {
}

/**
* Create a `Builder`.
*
* @param config the `DiscoveryConfiguration` to use to create the builder
*/
construct(DiscoveryConfiguration config) {
this.displayNameGenerator = config.displayNameGenerator;
}

DisplayNameGenerator displayNameGenerator = DisplayNameGenerator.Default;

Builder withDisplayNameGenerator(DisplayNameGenerator generator) {
displayNameGenerator = generator;
return this;
}

DiscoveryConfiguration build() {
return new DiscoveryConfiguration(displayNameGenerator);
}
}
}
91 changes: 91 additions & 0 deletions lib_xunit/src/main/x/xunit/DisplayNameGenerator.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import annotations.DisplayName;

/**
* A class that can generate display names for classes and methods.
*/
interface DisplayNameGenerator
extends Const {
/**
* Generate a non-empty, non-blank display name for the specified class.
*
* @param clz the class to generate a name for
*
* @return the display name for the class
*/
String nameForClass(Class clz);

/**
* Generate a non-empty, non-blank display name for the specified inner class.
*
* @param clz the class to generate a name for
*
* @return the display name for the class
*/
String nameForNestedClass(Class clz);

/**
* Generate a non-empty, non-blank display name for the specified method.
*
* @param clz the class the test method will be (or was) invoked on
* @param method the test method to generate a display name for
*
* @return the display name for the method
*/
String nameForMethod(Class clz, TestMethodOrFunction method);

/**
* Generate a non-empty, non-blank string representation for the parameters of the
* specified method.
*
* @param method the method to extract the parameter types from
*
* @return a string representation of all parameter types of the method
*/
static String parameterTypesAsString(TestMethodOrFunction method) {
StringBuffer buf = new StringBuffer().add('(');

EachParam: for (Parameter param : method.params) {
if (!EachParam.first) {
", ".appendTo(buf);
}
param.appendTo(buf);
}

return buf.add(')').toString();
}

/**
* A singleton instance of the default `DisplayNameGenerator`.
*/
static DisplayNameGenerator Default = new DefaultDisplayNameGenerator();

/**
* The default `DisplayNameGenerator` implementation.
*/
static const DefaultDisplayNameGenerator
implements DisplayNameGenerator {
@Override
String nameForClass(Class clz) {
if (clz.is(DisplayName)) {
return clz.name;
}
return clz.name;
}

@Override
String nameForNestedClass(Class clz) {
if (clz.is(DisplayName)) {
return clz.name;
}
return clz.name;
}

@Override
String nameForMethod(Class clz, TestMethodOrFunction method) {
if (method.is(DisplayName)) {
return method.name;
}
return clz.name + "." + method.name + parameterTypesAsString(method);
}
}
}
104 changes: 104 additions & 0 deletions lib_xunit/src/main/x/xunit/ExecutionContext.x
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import registry.ResourceRegistry;

/**
* Information about the current phase of execution of a test fixture.
* A test fixture could be a test method, or a test container.
*/
interface ExecutionContext {
/**
* The `UniqueId` of the current test fixture.
*/
@RO UniqueId uniqueId;

/**
* The display
*/
@RO String displayName;

/**
* The `ResourceRegistry` containing resources registered for this execution.
*/
@RO ResourceRegistry registry;

/**
* The `Module` associated to the current test fixture.
*/
@RO Module? testModule;

/**
* The `Package` associated to the current test fixture.
*/
@RO Package? testPackage;

/**
* The `Class` associated to the current test fixture.
*
* @return `True` iff the current test fixture is a `Class` or
* is the child of a `Class`.
* @return the `Class` associated to the current test fixture
*/
@RO Class? testClass;

/**
* The current test method.
*/
@RO TestMethodOrFunction? testMethod;

/**
* The current test fixture the test method will execute against.
*/
@RO Object? testFixture;

/**
* Any `Exception`s thrown during execution of the test lifecycle.
*/
@RO Exception? exception;

/**
* The `MethodExecutor` to use to execute tests.
*/
@RO MethodExecutor methodExecutor;

/**
* Invoke a `TestMethodOrFunction` using any registered `ParameterResolver` resources
* to resolve parameters for the function.
*
* @param method the `TestMethodOrFunction` to invoke
*
* @return the result of invoking the function
*/
Tuple invoke(TestMethodOrFunction method) {
if (method.is(Method)) {
assert testFixture != Null;
return methodExecutor.invoke(method.as(Method), testFixture, this);
}
return methodExecutor.invoke(method.as(Function), this);
}

/**
* Invoke a `TestMethodOrFunction` using any registered `ParameterResolver` resources
* to resolve parameters for the function and return the single result returned by the
* invocation.
*
* @param method the `TestMethodOrFunction` to invoke
*
* @return `True` iff the invocation returned a result
* @return the single result of invoking the function
*/
conditional Object invokeSingleResult(TestMethodOrFunction method) {
Tuple tuple = invoke(method);
if (tuple.size > 0) {
return True, tuple[0];
}
return False;
}

/**
* Create a default `ExecutionContext` from the specified model.
*
* @param model the `Model` to use to create the `ExecutionContext`
*/
static ExecutionContext create(Model model) {
return executor.DefaultExecutionContext.create(model);
}
}
Loading

0 comments on commit 5451e65

Please sign in to comment.