diff --git a/.project b/.project new file mode 100644 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + core + Project implabjs-core created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/build.gradle b/build.gradle --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,6 @@ plugins { // результатом будет версия '{num}.{distance}' где distance - расстояние от // текущей ревизии до ревизии с тэгом def tagDistance = 0; -def isRelease = false; if (!version) { @@ -35,25 +34,11 @@ if (hasProperty('versionSuffix') && vers version += "-$versionSuffix" } -if(!npmName) - npmName = name; - -if (hasProperty('release')) { - isRelease = (release != 'false') -} else { - isRelease = (tagDistance == 0); -} - -if(!["amd", "commonjs", "system", "umd", "es6", "esnext"].contains(jsmodule)) +if(! jsmodule in ["amd", "commonjs", "system", "umd", "es6", "esnext"]) throw new Exception("Invalid jsmodule specified: $jsmodule"); -if(!["es3", "es5", "es6", "es2016", "es2017", "esnext"].contains(target)) +if(! target in ["es3", "es5", "es6", "es2016", "es2017", "esnext"]) throw new Exception("Invalid target specified: $target") -def targetLibs = [ - "es3" : ["es5", "es2015.promise", "es2015.symbol", "dom", "scripthost"], - "es5" : ["es5", "es2015.promise", "es2015.symbol", "dom", "scripthost"] -]; - ext { packageName = "@$npmScope/$npmName" } @@ -61,25 +46,50 @@ ext { def jstarget = target; sources { + amd { + } + + cjs { + + } } typescript { compilerOptions { - lib = targetLibs[target] ?: [target, "dom"] + lib = [target, "dom", "scripthost"] + if (jstarget in ["es5", "es3"]) + lib += ["es2015.promise"] + target = jstarget module = jsmodule types = [] + declaration = true + listFiles = true + } tsLintCmd = "tslint" esLintCmd = "eslint" npmCmd = "npm" } +configureTsMain { + compilerOptions { + if (jstarget in ["es5", "es3"]) + lib += ["es2015.symbol", "es2015.iterable"] + } +} + +configureTsTest { + compilerOptions { + types += [ "node" ] + } +} + task printVersion { doLast { println "version: $version"; - println "isRelease: $isRelease, tagDistance: $tagDistance"; + println "tagDistance: $tagDistance"; println "packageName: $packageName"; println "bundle: ${npmPack.outputs.files.join(',')}"; println "target: $jstarget"; @@ -95,11 +105,6 @@ task clean { npmPackMeta { meta { - name = "@$npmScope/$npmName" + name = packageName } } - -task markRelease(type: Exec) { - onlyIf { tagDistance > 1 } - commandLine "hg", "tag", "v$version"; -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@types/node": { - "version": "10.12.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", - "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==", + "version": "8.10.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.55.tgz", + "integrity": "sha512-iZeh1EgupfmAAOASk580R1SL5lWF3CsBVgVH0395qyNF8fhO16xy1UwAav2PdGxIIsYRn7RzJgMGjdsvam6YYg==", "dev": true }, "@types/requirejs": { diff --git a/package.json b/package.json --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "tslib": "latest" }, "devDependencies": { - "@types/node": "latest", + "@types/node": "^8.0.0", "@types/requirejs": "latest", "@types/tape": "latest", "dojo": "^1.10.0", diff --git a/src/test/ts/ActivatableTests.ts b/src/test/ts/ActivatableTests.ts deleted file mode 100644 --- a/src/test/ts/ActivatableTests.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { MockActivationController } from "./mock/MockActivationController"; -import { SimpleActivatable } from "./mock/SimpleActivatable"; -import { test } from "./TestTraits"; - -test("simple activation", async t => { - - const a = new SimpleActivatable(); - t.false(a.isActive()); - - await a.activate(); - t.true(a.isActive()); - - await a.deactivate(); - t.false(a.isActive()); -}); - -test("controller activation", async t => { - - const a = new SimpleActivatable(); - const c = new MockActivationController(); - - t.false(a.isActive(), "the component is not active by default"); - t.assert(c.getActive() == null, "the activation controller doesn't have an active component by default"); - t.assert(a.getActivationController() == null, "the component doesn't have an activation controller by default"); - - t.comment("Active the component through the controller"); - await c.activate(a); - t.true(a.isActive(), "The component should successfully activate"); - t.equal(c.getActive(), a, "The controller should point to the activated component"); - t.equal(a.getActivationController(), c, "The component should point to the controller"); - - t.comment("Deactive the component throug the controller"); - await c.deactivate(); - - t.false(a.isActive(), "The component should successfully deactivate"); - t.equal(c.getActive(), null, "The controller shouldn't point to any component"); - t.equal(a.getActivationController(), c, "The componet should point to it's controller"); -}); - -test("handle error in onActivating", async t => { - const a = new SimpleActivatable(); - - a.onActivating = async () => { - throw new Error("Should fail"); - }; - - try { - await a.activate(); - t.fail("activation should fail"); - } catch { - } - - t.false(a.isActive(), "the component should remain inactive"); -}); diff --git a/src/test/ts/CancellationTests.ts b/src/test/ts/CancellationTests.ts deleted file mode 100644 --- a/src/test/ts/CancellationTests.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { Cancellation } from "@implab/core/Cancellation"; -import { delay } from "@implab/core/safe"; -import { test } from "./TestTraits"; - -test("standalone cancellation", async t => { - - let doCancel: (e) => void; - - const ct = new Cancellation(cancel => { - doCancel = cancel; - }); - - let counter = 0; - const reason = "BILL"; - - t.true(ct.isSupported(), "Cancellation must be supported"); - t.false(ct.isRequested(), "Cancellation shouldn't be requested"); - ct.throwIfRequested(); - t.pass("The exception shouldn't be thrown unless the cancellation is requested"); - - ct.register(() => counter++); - t.equals(counter, 0, "counter should be zero"); - - ct.register(() => counter++).destroy(); - - doCancel(reason); - - t.true(ct.isRequested(), "Cancellation should be requested"); - t.equals(counter, 1, "The registered callback should be triggered"); - - ct.register(() => counter++); - t.equals(counter, 2, "The callback should be triggered immediately"); - - let msg; - ct.register(e => msg = e); - t.equals(msg, reason, "The cancellation reason should be passed to callback"); - - try { - msg = null; - ct.throwIfRequested(); - t.fail("The exception should be thrown"); - } catch (e) { - msg = e; - } - t.equals(msg, reason, "The cancellation reason should be catched"); -}); - -test("async cancellation", async t => { - - const ct = new Cancellation(cancel => { - cancel("STOP!"); - }); - - try { - await delay(0, ct); - t.fail("Should thow the exception"); - } catch (e) { - t.equals(e, "STOP!", "Should throw the cancellation reason"); - } -}); - -test("cancel with external event", async t => { - const ct = new Cancellation(cancel => { - setTimeout(x => cancel("STOP!"), 0); - }); - - try { - await delay(10000, ct); - t.fail("Should thow the exception"); - } catch (e) { - t.equals(e, "STOP!", "Should throw the cancellation reason"); - } -}); - -test("operation normal flow", async t => { - - let htimeout; - const ct = new Cancellation(cancel => { - htimeout = setTimeout(() => cancel("STOP!"), 1000); - }); - - try { - await delay(0, ct); - t.pass("Should pass"); - } finally { - clearTimeout(htimeout); - } -}); diff --git a/src/test/ts/ContainerTests.ts b/src/test/ts/ContainerTests.ts deleted file mode 100644 --- a/src/test/ts/ContainerTests.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { test } from "./TestTraits"; -import { Container } from "@implab/core/di/Container"; -import { ReferenceDescriptor } from "@implab/core/di/ReferenceDescriptor"; -import { AggregateDescriptor } from "@implab/core/di/AggregateDescriptor"; -import { ValueDescriptor } from "@implab/core/di/ValueDescriptor"; -import { Foo } from "./mock/Foo"; -import { Bar } from "./mock/Bar"; -import { isNull } from "@implab/core/safe"; - -test("Container register/resolve tests", async t => { - const container = new Container(); - - const connection1 = "db://localhost"; - - t.throws( - () => container.register("bla-bla", "bla-bla"), - "Do not allow to register anything other than descriptors" - ); - - t.doesNotThrow( - () => container.register("connection", new ValueDescriptor(connection1)), - "register ValueDescriptor" - ); - - t.equals(container.resolve("connection"), connection1, "resolve string value"); - - t.doesNotThrow( - () => container.register( - "dbParams", - new AggregateDescriptor({ - timeout: 10, - connection: new ReferenceDescriptor({ name: "connection" }) - }) - ), - "register AggregateDescriptor" - ); - - const dbParams = container.resolve("dbParams"); - t.equals(dbParams.connection, connection1, "should get string value 'dbParams.connection'"); -}); - -test("Container configure/resolve tests", async t => { - - const container = new Container(); - - await container.configure({ - foo: { - $type: Foo - }, - - box: { - $type: Bar, - params: { - $dependency: "foo" - } - }, - - bar: { - $type: Bar, - params: { - db: { - provider: { - $dependency: "db" - } - } - } - } - }); - t.pass("should configure from js object"); - - const f1 = container.resolve("foo"); - - t.assert(!isNull(f1), "foo should be not null"); - - t.throws(() => container.resolve("bar"), "should not resolve dependency 'db'"); - -}); - -test("Load configuration from module", async t => { - const container = new Container(); - - await container.configure("./mock/config1", { contextRequire: require }); - t.pass("The configuration should load"); - - const f1 = container.resolve("foo"); - - t.assert(!isNull(f1), "foo should be not null"); - - const b1 = container.resolve("bar") as Bar; - - t.assert(!isNull(b1), "bar should not be null"); - t.assert(!isNull(b1.foo), "bar.foo should not be null"); -}); diff --git a/src/test/ts/ObservableTests.ts b/src/test/ts/ObservableTests.ts deleted file mode 100644 --- a/src/test/ts/ObservableTests.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { TraceSource, DebugLevel } from "@implab/core/log/TraceSource"; -import { Observable } from "@implab/core/Observable"; -import { IObservable } from "@implab/core/interfaces"; -import { delay } from "@implab/core/safe"; -import { test } from "./TestTraits"; - -const trace = TraceSource.get("ObservableTests"); - -test("events sequence example", async t => { - - let events: IObservable; - - const done = new Promise(resolve => { - events = new Observable(async (notify, fail, finish) => { - for (let i = 0; i < 10; i++) { - await delay(0); - notify(i); - } - finish(); - resolve(); - }); - }); - - let count = 0; - let complete = false; - events.on(x => count = count + x, null, () => complete = true); - - const first = await events.next(); - - t.equals(first, 0, "the first event"); - t.false(complete, "the sequence is not complete"); - - await done; - - t.equals(count, 45, "the summ of the evetns"); - t.true(complete, "the sequence is complete"); -}); - -test("event sequence termination", async t => { - let events: IObservable; - - const done = new Promise(resolve => { - events = new Observable(async (notify, fail, complete) => { - await delay(0); - notify(1); - complete(); - notify(2); - complete(); - fail("Sequence terminated"); - resolve(); - }); - }); - - let count = 0; - events.on(() => {}, e => count++, () => count++); - - const first = await events.next(); - t.equals(first, 1, "the first message"); - try { - await events.next(); - t.fail("shoud throw an exception"); - } catch (e) { - t.pass("the sequence is terminated"); - } - - await done; - - t.equals(count, 1, "the sequence must be terminated once"); -}); diff --git a/src/test/ts/SafeTests.ts b/src/test/ts/SafeTests.ts deleted file mode 100644 --- a/src/test/ts/SafeTests.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { Cancellation } from "@implab/core/Cancellation"; -import { first, isPromise, firstWhere, delay, nowait } from "@implab/core/safe"; -import { test } from "./TestTraits"; - -test("await delay test", async t => { - // schedule delay - let resolved = false; - let res = delay(0).then(() => resolved = true); - - t.false(resolved, "the delay should be async"); - - await res; - t.pass("await delay"); - - // create cancellation token - let cancel: (e?: any) => void; - const ct = new Cancellation(c => cancel = c); - - // schedule delay - resolved = false; - res = delay(0, ct).then(() => resolved = true); - - t.false(resolved, "created delay with ct"); - - // cancel - cancel(); - - try { - await res; - t.fail("the delay should fail when it is cancelled"); - } catch { - t.pass("the delay is cancelled"); - } - - t.throws(() => { - // try schedule delay after the cancellation is requested - nowait(delay(0, ct)); - }, "Should throw if cancelled before start"); -}); - -test("sequemce test", async t => { - const sequence = ["a", "b", "c"]; - const empty = []; - - // synchronous tests - t.equals(first(sequence), "a", "Should return the first element"); - t.equals(firstWhere(sequence, x => x === "b"), "b", "Should get the second element"); - - let v: string; - let e: Error; - first(sequence, x => v = x); - t.equal(v, "a", "The callback should be called for the first element"); - firstWhere(sequence, x => x === "b", x => v = x); - t.equal(v, "b", "The callback should be called for the second element"); - - t.throws(() => { - first(empty); - }, "Should throw when the sequence is empty"); - - t.throws(() => { - firstWhere(empty, x => x === "b"); - }, "Should throw when the sequence is empty"); - - t.throws(() => { - first(empty, x => v = x); - }, "Should throw when the sequence is empty"); - - t.throws(() => { - firstWhere(empty, x => x === "b", x => v = x); - }, "Should throw when the sequence is empty"); - - t.throws(() => { - firstWhere(sequence, x => x === "z"); - }, "Should throw when the element isn't found"); - - t.throws(() => { - firstWhere(sequence, x => x === "z", x => v = x); - }, "Should throw when the element isn't found"); - - first(empty, null, x => e = x); - t.true(e, "The errorback should be called for the empty sequence"); - - // async tests - const asyncSequence = Promise.resolve(sequence); - const asyncEmptySequence = Promise.resolve(empty); - - const promise = first(asyncSequence); - t.true(isPromise(promise), "Should return promise"); - - v = await promise; - t.equal(v, "a", "Should return the first element"); - - v = await new Promise(resolve => first(asyncSequence, resolve)); - t.equal(v, "a", "The callback should be called for the first element"); -}); diff --git a/src/test/ts/TestTraits.ts b/src/test/ts/TestTraits.ts deleted file mode 100644 --- a/src/test/ts/TestTraits.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { IObservable, ICancellation, IDestroyable } from "@implab/core/interfaces"; -import { Cancellation } from "@implab/core/Cancellation"; -import { TraceEvent, LogLevel, WarnLevel, DebugLevel, TraceSource } from "@implab/core/log/TraceSource"; -import * as tape from "tape"; -import { argumentNotNull, destroy } from "@implab/core/safe"; - -export class TapeWriter implements IDestroyable { - private readonly _tape: tape.Test; - - private readonly _subscriptions = new Array(); - private _destroyed; - - constructor(t: tape.Test) { - argumentNotNull(t, "tape"); - this._tape = t; - } - - writeEvents(source: IObservable, ct: ICancellation = Cancellation.none) { - if (!this._destroyed) { - const subscription = source.on(this.writeEvent.bind(this)); - if (ct.isSupported()) { - ct.register(subscription.destroy.bind(subscription)); - } - this._subscriptions.push(subscription); - } - } - - writeEvent(next: TraceEvent) { - if (next.level >= DebugLevel) { - this._tape.comment(`DEBUG ${next.source.id} ${next}`); - } else if (next.level >= LogLevel) { - this._tape.comment(`LOG ${next.source.id} ${next}`); - } else if (next.level >= WarnLevel) { - this._tape.comment(`WARN ${next.source.id} ${next}`); - } else { - this._tape.comment(`ERROR ${next.source.id} ${next}`); - } - } - - destroy() { - this._subscriptions.forEach(destroy); - } -} - -export function test(name: string, cb: (t: tape.Test, trace: TraceSource) => any) { - tape(name, async t => { - const writer = new TapeWriter(t); - - // this trace is not announced through the TraceSource global registry - const trace = new TraceSource(name); - trace.level = DebugLevel; - writer.writeEvents(trace.events); - - const h = TraceSource.on(ts => { - ts.level = DebugLevel; - writer.writeEvents(ts.events); - }); - - try { - await cb(t, trace); - } catch (e) { - - // verbose error information - // tslint:disable-next-line - console.error(e); - t.fail(e); - - } finally { - t.end(); - destroy(writer); - destroy(h); - } - }); -} diff --git a/src/test/ts/TextTests.ts b/src/test/ts/TextTests.ts deleted file mode 100644 --- a/src/test/ts/TextTests.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { StringBuilder } from "@implab/core/text/StringBuilder"; -import { test } from "./TestTraits"; -import { MockConsole } from "./mock/MockConsole"; -import { ConsoleWriter } from "@implab/core/log/ConsoleWriter"; - -test("String builder", async t => { - const sb = new StringBuilder(); - - sb.write("hello"); - t.equals(sb.toString(), "hello", "Write simple text"); - - sb.write(", "); - sb.write("world!"); - t.equals(sb.toString(), "hello, world!", "Append text"); - - sb.clear(); - t.equals(sb.toString(), "", "Clear"); - - sb.write(1); - t.equals(sb.toString(), "1", "Write number"); - - sb.clear(); - sb.writeValue(0.123); - t.equals(sb.toString(), "0.123", "Format number"); - - sb.clear(); - sb.writeValue(new Date("2019-01-02T00:00:00.000Z")); - t.equals(sb.toString(), "2019-01-02T00:00:00.000Z", "Format date (ISO)"); - - sb.clear(); - sb.write("{0}", "hello"); - t.equals(sb.toString(), "hello", "Simple format text"); - - sb.write(", {0}!", "world"); - t.equals(sb.toString(), "hello, world!", "Append formatted text"); - - sb.clear(); - sb.write("abc: {0:json}; {0.length}; {0.1} {{olo}}", ["a", "b", "c"]); - t.equals(sb.toString(), 'abc: [\n "a",\n "b",\n "c"\n]; 3; b {olo}', "Format string with spec"); - - sb.clear(); - t.throws(() => sb.write("}", 0), "Should die on bad format: '}'"); - t.throws(() => sb.write("{", 0), "Should die on bad format: '{'"); - t.throws(() => sb.write("{}", 0), "Should die on bad format: '{}'"); - t.throws(() => sb.write("{:}", 0), "Should die on bad format: '{:}'"); - t.throws(() => sb.write("{{0}", 0), "Should die on bad format: '{{0}'"); - -}); - -test("ConsoleWriter", t => { - const mockConsole = new MockConsole(); - const writer = new ConsoleWriter(mockConsole); - - writer.setLogLevel("log"); - - writer.writeLine("Hello, world!"); - - t.equals(mockConsole.getBuffer().length, 1, "One line should be written"); - t.equals(mockConsole.getBuffer()[0].level, "log", "LogLevel should be 'log'"); - t.deepEqual(mockConsole.getBuffer()[0].data, ["Hello, world!"], "The buffer should contain single string"); - - mockConsole.clear(); - writer.setLogLevel("debug"); - writer.write("Bring "); - writer.write("the {0}!", "light"); - t.equals(mockConsole.getBuffer().length, 0, "No line should be written"); - writer.writeLine(); - - t.equals(mockConsole.getBuffer().length, 1, "One line should be written"); - t.equals(mockConsole.getBuffer()[0].level, "debug", "LogLevel should be 'log'"); - t.deepEqual(mockConsole.getBuffer()[0].data, ["Bring the light!"], "Should concatenate string parts together"); - - mockConsole.clear(); - writer.writeLine("It's {0} o'clock, lets have some {1}!", { h: 5}, { title: "tee" }); - - t.deepEqual(mockConsole.getBuffer()[0].data, ["It's ", { h: 5}, " o'clock, lets have some ", { title: "tee" }, "!"], "Non string parts should be psassed as is"); - - mockConsole.clear(); - writer.writeLine("{0} or {1} to {2}", {i: 25}, 6, 4); - t.deepEqual(mockConsole.getBuffer()[0].data, [{i: 25}, " or 6 to 4"], "25 or 6 to 4"); - - mockConsole.clear(); - writer.writeLine("{0} or {1} to {2}! Let's have some {3}", 25, 6, 4, { product: "tee" } ); - t.deepEqual(mockConsole.getBuffer()[0].data, ["25 or 6 to 4! Let's have some ", { product: "tee" }], "Should handle many text chunks and object at the end"); - -}); diff --git a/src/test/ts/TraceSourceTests.ts b/src/test/ts/TraceSourceTests.ts deleted file mode 100644 --- a/src/test/ts/TraceSourceTests.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { TraceSource, DebugLevel } from "@implab/core/log/TraceSource"; -import * as tape from "tape"; -import { TapeWriter, test } from "./TestTraits"; -import { MockConsole } from "./mock/MockConsole"; -import { ConsoleLogger } from "@implab/core/log/writers/ConsoleLogger"; -import { ConsoleWriter } from "@implab/core/log/ConsoleWriter"; - -const sourceId = "test/TraceSourceTests"; - -tape("trace message", t => { - const trace = TraceSource.get(sourceId); - - trace.level = DebugLevel; - - const h = trace.events.on(ev => { - t.equal(ev.source, trace, "sender should be the current trace source"); - t.equal(ev.level, DebugLevel, "level should be debug level"); - t.equal(ev.toString(), "Hello, World!", "The message should be a formatted message"); - - t.end(); - }); - - trace.debug("Hello, {0}!", "World"); - - h.destroy(); -}); - -tape("trace event", t => { - const trace = TraceSource.get(sourceId); - - trace.level = DebugLevel; - - const event = { - name: "custom event" - }; - - const h = trace.events.on(ev => { - t.equal(ev.source, trace, "sender should be the current trace source"); - t.equal(ev.level, DebugLevel, "level should be debug level"); - t.equal(ev.message, event, "The message should be the specified object"); - - t.end(); - }); - - trace.traceEvent(DebugLevel, event); - - h.destroy(); -}); - -tape("tape comment writer", async t => { - const writer = new TapeWriter(t); - - TraceSource.on(ts => { - writer.writeEvents(ts.events); - }); - - const trace = TraceSource.get(sourceId); - trace.level = DebugLevel; - - trace.log("Hello, {0}!", "World"); - trace.log("Multi\n line"); - trace.warn("Look at me!"); - trace.error("DIE!"); - - writer.destroy(); - - trace.log("You shouldn't see it!"); - - t.comment("DONE"); - - t.end(); -}); - -test("console writer", (t, trace) => { - - const mockConsole = new MockConsole(); - const writer = new ConsoleWriter(mockConsole); - const consoleLog = new ConsoleLogger(writer); - consoleLog.writeEvents(trace.events); - - trace.log("Hello, world!"); - t.deepEqual(mockConsole.getLine(0), ["console writer: Hello, world!"], "Log one string"); - - trace.log({ foo: "bar" }); - t.deepEqual(mockConsole.getLine(1), ["console writer: ", { foo: "bar" }], "Log an object"); - - trace.log("json: {0:json}", { foo: "bar" }); - t.deepEqual(mockConsole.getLine(2), ['console writer: json: {\n "foo": "bar"\n}'], "should convert to string substitutions with spec"); -}); diff --git a/src/test/ts/dummy.ts b/src/test/ts/dummy.ts deleted file mode 100644 --- a/src/test/ts/dummy.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as tape from "tape"; -import { Uuid } from "@implab/core/Uuid"; - -tape("simple", t => { - t.pass("sync assert"); - setTimeout(() => { - t.pass("async assert"); - t.comment(Uuid()); - t.ok(Uuid() !== Uuid()); - // end should be called after the last assertion - t.end(); - }, 100); -}); diff --git a/src/test/ts/mock/MockActivationController.ts b/src/test/ts/mock/MockActivationController.ts --- a/src/test/ts/mock/MockActivationController.ts +++ b/src/test/ts/mock/MockActivationController.ts @@ -1,5 +1,5 @@ -import { IActivatable, ICancellation, IActivationController } from "@implab/core/interfaces"; -import { Cancellation } from "@implab/core/Cancellation"; +import { IActivatable, ICancellation, IActivationController } from "../interfaces"; +import { Cancellation } from "../Cancellation"; export class MockActivationController implements IActivationController { diff --git a/src/test/ts/mock/MockConsole.ts b/src/test/ts/mock/MockConsole.ts --- a/src/test/ts/mock/MockConsole.ts +++ b/src/test/ts/mock/MockConsole.ts @@ -1,4 +1,4 @@ -import {NullConsole} from "@implab/core/log/NullConsole"; +import {NullConsole} from "../log/NullConsole"; interface ConsoleLineData { level: string; diff --git a/src/test/ts/mock/SimpleActivatable.ts b/src/test/ts/mock/SimpleActivatable.ts --- a/src/test/ts/mock/SimpleActivatable.ts +++ b/src/test/ts/mock/SimpleActivatable.ts @@ -1,5 +1,5 @@ -import { AsyncComponent } from "@implab/core/components/AsyncComponent"; -import { ActivatableMixin } from "@implab/core/components/ActivatableMixin"; +import { AsyncComponent } from "../components/AsyncComponent"; +import { ActivatableMixin } from "../components/ActivatableMixin"; export class SimpleActivatable extends ActivatableMixin(AsyncComponent) { diff --git a/src/test/ts/tests/ActivatableTests.ts b/src/test/ts/tests/ActivatableTests.ts new file mode 100644 --- /dev/null +++ b/src/test/ts/tests/ActivatableTests.ts @@ -0,0 +1,54 @@ +import { MockActivationController } from "../mock/MockActivationController"; +import { SimpleActivatable } from "../mock/SimpleActivatable"; +import { test } from "./TestTraits"; + +test("simple activation", async t => { + + const a = new SimpleActivatable(); + t.false(a.isActive()); + + await a.activate(); + t.true(a.isActive()); + + await a.deactivate(); + t.false(a.isActive()); +}); + +test("controller activation", async t => { + + const a = new SimpleActivatable(); + const c = new MockActivationController(); + + t.false(a.isActive(), "the component is not active by default"); + t.assert(c.getActive() == null, "the activation controller doesn't have an active component by default"); + t.assert(a.getActivationController() == null, "the component doesn't have an activation controller by default"); + + t.comment("Active the component through the controller"); + await c.activate(a); + t.true(a.isActive(), "The component should successfully activate"); + t.equal(c.getActive(), a, "The controller should point to the activated component"); + t.equal(a.getActivationController(), c, "The component should point to the controller"); + + t.comment("Deactive the component throug the controller"); + await c.deactivate(); + + t.false(a.isActive(), "The component should successfully deactivate"); + t.equal(c.getActive(), null, "The controller shouldn't point to any component"); + t.equal(a.getActivationController(), c, "The componet should point to it's controller"); +}); + +test("handle error in onActivating", async t => { + const a = new SimpleActivatable(); + + a.onActivating = async () => { + throw new Error("Should fail"); + }; + + try { + await a.activate(); + t.fail("activation should fail"); + } catch { + } + + t.false(a.isActive(), "the component should remain inactive"); +}); diff --git a/src/test/ts/tests/CancellationTests.ts b/src/test/ts/tests/CancellationTests.ts new file mode 100644 --- /dev/null +++ b/src/test/ts/tests/CancellationTests.ts @@ -0,0 +1,88 @@ +import { Cancellation } from "../Cancellation"; +import { delay } from "../safe"; +import { test } from "./TestTraits"; + +test("standalone cancellation", async t => { + + let doCancel: (e) => void; + + const ct = new Cancellation(cancel => { + doCancel = cancel; + }); + + let counter = 0; + const reason = "BILL"; + + t.true(ct.isSupported(), "Cancellation must be supported"); + t.false(ct.isRequested(), "Cancellation shouldn't be requested"); + ct.throwIfRequested(); + t.pass("The exception shouldn't be thrown unless the cancellation is requested"); + + ct.register(() => counter++); + t.equals(counter, 0, "counter should be zero"); + + ct.register(() => counter++).destroy(); + + doCancel(reason); + + t.true(ct.isRequested(), "Cancellation should be requested"); + t.equals(counter, 1, "The registered callback should be triggered"); + + ct.register(() => counter++); + t.equals(counter, 2, "The callback should be triggered immediately"); + + let msg; + ct.register(e => msg = e); + t.equals(msg, reason, "The cancellation reason should be passed to callback"); + + try { + msg = null; + ct.throwIfRequested(); + t.fail("The exception should be thrown"); + } catch (e) { + msg = e; + } + t.equals(msg, reason, "The cancellation reason should be catched"); +}); + +test("async cancellation", async t => { + + const ct = new Cancellation(cancel => { + cancel("STOP!"); + }); + + try { + await delay(0, ct); + t.fail("Should thow the exception"); + } catch (e) { + t.equals(e, "STOP!", "Should throw the cancellation reason"); + } +}); + +test("cancel with external event", async t => { + const ct = new Cancellation(cancel => { + setTimeout(x => cancel("STOP!"), 0); + }); + + try { + await delay(10000, ct); + t.fail("Should thow the exception"); + } catch (e) { + t.equals(e, "STOP!", "Should throw the cancellation reason"); + } +}); + +test("operation normal flow", async t => { + + let htimeout; + const ct = new Cancellation(cancel => { + htimeout = setTimeout(() => cancel("STOP!"), 1000); + }); + + try { + await delay(0, ct); + t.pass("Should pass"); + } finally { + clearTimeout(htimeout); + } +}); diff --git a/src/test/ts/tests/ContainerTests.ts b/src/test/ts/tests/ContainerTests.ts new file mode 100644 --- /dev/null +++ b/src/test/ts/tests/ContainerTests.ts @@ -0,0 +1,93 @@ +import { test } from "./TestTraits"; +import { Container } from "../di/Container"; +import { ReferenceDescriptor } from "../di/ReferenceDescriptor"; +import { AggregateDescriptor } from "../di/AggregateDescriptor"; +import { ValueDescriptor } from "../di/ValueDescriptor"; +import { Foo } from "../mock/Foo"; +import { Bar } from "../mock/Bar"; +import { isNull } from "../safe"; + +test("Container register/resolve tests", async t => { + const container = new Container(); + + const connection1 = "db://localhost"; + + t.throws( + () => container.register("bla-bla", "bla-bla"), + "Do not allow to register anything other than descriptors" + ); + + t.doesNotThrow( + () => container.register("connection", new ValueDescriptor(connection1)), + "register ValueDescriptor" + ); + + t.equals(container.resolve("connection"), connection1, "resolve string value"); + + t.doesNotThrow( + () => container.register( + "dbParams", + new AggregateDescriptor({ + timeout: 10, + connection: new ReferenceDescriptor({ name: "connection" }) + }) + ), + "register AggregateDescriptor" + ); + + const dbParams = container.resolve("dbParams"); + t.equals(dbParams.connection, connection1, "should get string value 'dbParams.connection'"); +}); + +test("Container configure/resolve tests", async t => { + + const container = new Container(); + + await container.configure({ + foo: { + $type: Foo + }, + + box: { + $type: Bar, + params: { + $dependency: "foo" + } + }, + + bar: { + $type: Bar, + params: { + db: { + provider: { + $dependency: "db" + } + } + } + } + }); + t.pass("should configure from js object"); + + const f1 = container.resolve("foo"); + + t.assert(!isNull(f1), "foo should be not null"); + + t.throws(() => container.resolve("bar"), "should not resolve dependency 'db'"); + +}); + +test("Load configuration from module", async t => { + const container = new Container(); + + await container.configure("./mock/config1", { contextRequire: require }); + t.pass("The configuration should load"); + + const f1 = container.resolve("foo"); + + t.assert(!isNull(f1), "foo should be not null"); + + const b1 = container.resolve("bar") as Bar; + + t.assert(!isNull(b1), "bar should not be null"); + t.assert(!isNull(b1.foo), "bar.foo should not be null"); +}); diff --git a/src/test/ts/tests/ObservableTests.ts b/src/test/ts/tests/ObservableTests.ts new file mode 100644 --- /dev/null +++ b/src/test/ts/tests/ObservableTests.ts @@ -0,0 +1,69 @@ +import { TraceSource } from "../log/TraceSource"; +import { Observable } from "../Observable"; +import { IObservable } from "../interfaces"; +import { delay } from "../safe"; +import { test } from "./TestTraits"; + +const trace = TraceSource.get("ObservableTests"); + +test("events sequence example", async t => { + + let events: IObservable; + + const done = new Promise(resolve => { + events = new Observable(async (notify, fail, finish) => { + for (let i = 0; i < 10; i++) { + await delay(0); + notify(i); + } + finish(); + resolve(); + }); + }); + + let count = 0; + let complete = false; + events.on(x => count = count + x, null, () => complete = true); + + const first = await events.next(); + + t.equals(first, 0, "the first event"); + t.false(complete, "the sequence is not complete"); + + await done; + + t.equals(count, 45, "the summ of the evetns"); + t.true(complete, "the sequence is complete"); +}); + +test("event sequence termination", async t => { + let events: IObservable; + + const done = new Promise(resolve => { + events = new Observable(async (notify, fail, complete) => { + await delay(0); + notify(1); + complete(); + notify(2); + complete(); + fail("Sequence terminated"); + resolve(); + }); + }); + + let count = 0; + events.on(() => {}, e => count++, () => count++); + + const first = await events.next(); + t.equals(first, 1, "the first message"); + try { + await events.next(); + t.fail("shoud throw an exception"); + } catch (e) { + t.pass("the sequence is terminated"); + } + + await done; + + t.equals(count, 1, "the sequence must be terminated once"); +}); diff --git a/src/test/ts/tests/SafeTests.ts b/src/test/ts/tests/SafeTests.ts new file mode 100644 --- /dev/null +++ b/src/test/ts/tests/SafeTests.ts @@ -0,0 +1,95 @@ +import { Cancellation } from "../Cancellation"; +import { first, isPromise, firstWhere, delay, nowait } from "../safe"; +import { test } from "./TestTraits"; + +test("await delay test", async t => { + // schedule delay + let resolved = false; + let res = delay(0).then(() => resolved = true); + + t.false(resolved, "the delay should be async"); + + await res; + t.pass("await delay"); + + // create cancellation token + let cancel: (e?: any) => void; + const ct = new Cancellation(c => cancel = c); + + // schedule delay + resolved = false; + res = delay(0, ct).then(() => resolved = true); + + t.false(resolved, "created delay with ct"); + + // cancel + cancel(); + + try { + await res; + t.fail("the delay should fail when it is cancelled"); + } catch { + t.pass("the delay is cancelled"); + } + + t.throws(() => { + // try schedule delay after the cancellation is requested + nowait(delay(0, ct)); + }, "Should throw if cancelled before start"); +}); + +test("sequemce test", async t => { + const sequence = ["a", "b", "c"]; + const empty = []; + + // synchronous tests + t.equals(first(sequence), "a", "Should return the first element"); + t.equals(firstWhere(sequence, x => x === "b"), "b", "Should get the second element"); + + let v: string; + let e: Error; + first(sequence, x => v = x); + t.equal(v, "a", "The callback should be called for the first element"); + firstWhere(sequence, x => x === "b", x => v = x); + t.equal(v, "b", "The callback should be called for the second element"); + + t.throws(() => { + first(empty); + }, "Should throw when the sequence is empty"); + + t.throws(() => { + firstWhere(empty, x => x === "b"); + }, "Should throw when the sequence is empty"); + + t.throws(() => { + first(empty, x => v = x); + }, "Should throw when the sequence is empty"); + + t.throws(() => { + firstWhere(empty, x => x === "b", x => v = x); + }, "Should throw when the sequence is empty"); + + t.throws(() => { + firstWhere(sequence, x => x === "z"); + }, "Should throw when the element isn't found"); + + t.throws(() => { + firstWhere(sequence, x => x === "z", x => v = x); + }, "Should throw when the element isn't found"); + + first(empty, null, x => e = x); + t.true(e, "The errorback should be called for the empty sequence"); + + // async tests + const asyncSequence = Promise.resolve(sequence); + const asyncEmptySequence = Promise.resolve(empty); + + const promise = first(asyncSequence); + t.true(isPromise(promise), "Should return promise"); + + v = await promise; + t.equal(v, "a", "Should return the first element"); + + v = await new Promise(resolve => first(asyncSequence, resolve)); + t.equal(v, "a", "The callback should be called for the first element"); +}); diff --git a/src/test/ts/tests/TestTraits.ts b/src/test/ts/tests/TestTraits.ts new file mode 100644 --- /dev/null +++ b/src/test/ts/tests/TestTraits.ts @@ -0,0 +1,74 @@ +import { IObservable, ICancellation, IDestroyable } from "../interfaces"; +import { Cancellation } from "../Cancellation"; +import { TraceEvent, LogLevel, WarnLevel, DebugLevel, TraceSource } from "../log/TraceSource"; +import * as tape from "tape"; +import { argumentNotNull, destroy } from "../safe"; + +export class TapeWriter implements IDestroyable { + private readonly _tape: tape.Test; + + private readonly _subscriptions = new Array(); + private _destroyed; + + constructor(t: tape.Test) { + argumentNotNull(t, "tape"); + this._tape = t; + } + + writeEvents(source: IObservable, ct: ICancellation = Cancellation.none) { + if (!this._destroyed) { + const subscription = source.on(this.writeEvent.bind(this)); + if (ct.isSupported()) { + ct.register(subscription.destroy.bind(subscription)); + } + this._subscriptions.push(subscription); + } + } + + writeEvent(next: TraceEvent) { + if (next.level >= DebugLevel) { + this._tape.comment(`DEBUG ${next.source.id} ${next}`); + } else if (next.level >= LogLevel) { + this._tape.comment(`LOG ${next.source.id} ${next}`); + } else if (next.level >= WarnLevel) { + this._tape.comment(`WARN ${next.source.id} ${next}`); + } else { + this._tape.comment(`ERROR ${next.source.id} ${next}`); + } + } + + destroy() { + this._subscriptions.forEach(destroy); + } +} + +export function test(name: string, cb: (t: tape.Test, trace: TraceSource) => any) { + tape(name, async t => { + const writer = new TapeWriter(t); + + // this trace is not announced through the TraceSource global registry + const trace = new TraceSource(name); + trace.level = DebugLevel; + writer.writeEvents(trace.events); + + const h = TraceSource.on(ts => { + ts.level = DebugLevel; + writer.writeEvents(ts.events); + }); + + try { + await cb(t, trace); + } catch (e) { + + // verbose error information + // tslint:disable-next-line + console.error(e); + t.fail(e); + + } finally { + t.end(); + destroy(writer); + destroy(h); + } + }); +} diff --git a/src/test/ts/tests/TextTests.ts b/src/test/ts/tests/TextTests.ts new file mode 100644 --- /dev/null +++ b/src/test/ts/tests/TextTests.ts @@ -0,0 +1,86 @@ +import { StringBuilder } from "../text/StringBuilder"; +import { test } from "./TestTraits"; +import { MockConsole } from "../mock/MockConsole"; +import { ConsoleWriter } from "../log/ConsoleWriter"; + +test("String builder", async t => { + const sb = new StringBuilder(); + + sb.write("hello"); + t.equals(sb.toString(), "hello", "Write simple text"); + + sb.write(", "); + sb.write("world!"); + t.equals(sb.toString(), "hello, world!", "Append text"); + + sb.clear(); + t.equals(sb.toString(), "", "Clear"); + + sb.write(1); + t.equals(sb.toString(), "1", "Write number"); + + sb.clear(); + sb.writeValue(0.123); + t.equals(sb.toString(), "0.123", "Format number"); + + sb.clear(); + sb.writeValue(new Date("2019-01-02T00:00:00.000Z")); + t.equals(sb.toString(), "2019-01-02T00:00:00.000Z", "Format date (ISO)"); + + sb.clear(); + sb.write("{0}", "hello"); + t.equals(sb.toString(), "hello", "Simple format text"); + + sb.write(", {0}!", "world"); + t.equals(sb.toString(), "hello, world!", "Append formatted text"); + + sb.clear(); + sb.write("abc: {0:json}; {0.length}; {0.1} {{olo}}", ["a", "b", "c"]); + t.equals(sb.toString(), 'abc: [\n "a",\n "b",\n "c"\n]; 3; b {olo}', "Format string with spec"); + + sb.clear(); + t.throws(() => sb.write("}", 0), "Should die on bad format: '}'"); + t.throws(() => sb.write("{", 0), "Should die on bad format: '{'"); + t.throws(() => sb.write("{}", 0), "Should die on bad format: '{}'"); + t.throws(() => sb.write("{:}", 0), "Should die on bad format: '{:}'"); + t.throws(() => sb.write("{{0}", 0), "Should die on bad format: '{{0}'"); + +}); + +test("ConsoleWriter", t => { + const mockConsole = new MockConsole(); + const writer = new ConsoleWriter(mockConsole); + + writer.setLogLevel("log"); + + writer.writeLine("Hello, world!"); + + t.equals(mockConsole.getBuffer().length, 1, "One line should be written"); + t.equals(mockConsole.getBuffer()[0].level, "log", "LogLevel should be 'log'"); + t.deepEqual(mockConsole.getBuffer()[0].data, ["Hello, world!"], "The buffer should contain single string"); + + mockConsole.clear(); + writer.setLogLevel("debug"); + writer.write("Bring "); + writer.write("the {0}!", "light"); + t.equals(mockConsole.getBuffer().length, 0, "No line should be written"); + writer.writeLine(); + + t.equals(mockConsole.getBuffer().length, 1, "One line should be written"); + t.equals(mockConsole.getBuffer()[0].level, "debug", "LogLevel should be 'log'"); + t.deepEqual(mockConsole.getBuffer()[0].data, ["Bring the light!"], "Should concatenate string parts together"); + + mockConsole.clear(); + writer.writeLine("It's {0} o'clock, lets have some {1}!", { h: 5}, { title: "tee" }); + + t.deepEqual(mockConsole.getBuffer()[0].data, ["It's ", { h: 5}, " o'clock, lets have some ", { title: "tee" }, "!"], "Non string parts should be psassed as is"); + + mockConsole.clear(); + writer.writeLine("{0} or {1} to {2}", {i: 25}, 6, 4); + t.deepEqual(mockConsole.getBuffer()[0].data, [{i: 25}, " or 6 to 4"], "25 or 6 to 4"); + + mockConsole.clear(); + writer.writeLine("{0} or {1} to {2}! Let's have some {3}", 25, 6, 4, { product: "tee" } ); + t.deepEqual(mockConsole.getBuffer()[0].data, ["25 or 6 to 4! Let's have some ", { product: "tee" }], "Should handle many text chunks and object at the end"); + +}); diff --git a/src/test/ts/tests/TraceSourceTests.ts b/src/test/ts/tests/TraceSourceTests.ts new file mode 100644 --- /dev/null +++ b/src/test/ts/tests/TraceSourceTests.ts @@ -0,0 +1,89 @@ +import { TraceSource, DebugLevel } from "../log/TraceSource"; +import * as tape from "tape"; +import { TapeWriter, test } from "./TestTraits"; +import { MockConsole } from "../mock/MockConsole"; +import { ConsoleLogger } from "../log/writers/ConsoleLogger"; +import { ConsoleWriter } from "../log/ConsoleWriter"; + +const sourceId = "test/TraceSourceTests"; + +tape("trace message", t => { + const trace = TraceSource.get(sourceId); + + trace.level = DebugLevel; + + const h = trace.events.on(ev => { + t.equal(ev.source, trace, "sender should be the current trace source"); + t.equal(ev.level, DebugLevel, "level should be debug level"); + t.equal(ev.toString(), "Hello, World!", "The message should be a formatted message"); + + t.end(); + }); + + trace.debug("Hello, {0}!", "World"); + + h.destroy(); +}); + +tape("trace event", t => { + const trace = TraceSource.get(sourceId); + + trace.level = DebugLevel; + + const event = { + name: "custom event" + }; + + const h = trace.events.on(ev => { + t.equal(ev.source, trace, "sender should be the current trace source"); + t.equal(ev.level, DebugLevel, "level should be debug level"); + t.equal(ev.message, event, "The message should be the specified object"); + + t.end(); + }); + + trace.traceEvent(DebugLevel, event); + + h.destroy(); +}); + +tape("tape comment writer", async t => { + const writer = new TapeWriter(t); + + TraceSource.on(ts => { + writer.writeEvents(ts.events); + }); + + const trace = TraceSource.get(sourceId); + trace.level = DebugLevel; + + trace.log("Hello, {0}!", "World"); + trace.log("Multi\n line"); + trace.warn("Look at me!"); + trace.error("DIE!"); + + writer.destroy(); + + trace.log("You shouldn't see it!"); + + t.comment("DONE"); + + t.end(); +}); + +test("console writer", (t, trace) => { + + const mockConsole = new MockConsole(); + const writer = new ConsoleWriter(mockConsole); + const consoleLog = new ConsoleLogger(writer); + consoleLog.writeEvents(trace.events); + + trace.log("Hello, world!"); + t.deepEqual(mockConsole.getLine(0), ["console writer: Hello, world!"], "Log one string"); + + trace.log({ foo: "bar" }); + t.deepEqual(mockConsole.getLine(1), ["console writer: ", { foo: "bar" }], "Log an object"); + + trace.log("json: {0:json}", { foo: "bar" }); + t.deepEqual(mockConsole.getLine(2), ['console writer: json: {\n "foo": "bar"\n}'], "should convert to string substitutions with spec"); +}); diff --git a/src/test/ts/tests/dummy.ts b/src/test/ts/tests/dummy.ts new file mode 100644 --- /dev/null +++ b/src/test/ts/tests/dummy.ts @@ -0,0 +1,13 @@ +import * as tape from "tape"; +import { Uuid } from "../Uuid"; + +tape("simple", t => { + t.pass("sync assert"); + setTimeout(() => { + t.pass("async assert"); + t.comment(Uuid()); + t.ok(Uuid() !== Uuid()); + // end should be called after the last assertion + t.end(); + }, 100); +}); diff --git a/src/test/tsconfig.json b/src/test/tsconfig.json --- a/src/test/tsconfig.json +++ b/src/test/tsconfig.json @@ -3,11 +3,10 @@ "compilerOptions": { "rootDir": "ts", "baseUrl": ".", - "paths": { - "@implab/core/*": [ - "../../build/dist/*" - ] - } + "rootDirs": [ + "ts", + "../main/ts" + ] }, "include" : [ "ts/**/*.ts"