##// END OF EJS Templates
StringBuilder, TextWriter, ConsoleWriter tests
cin -
r82:025f02eff3b2 v1.3.0 default
parent child
Show More
@@ -0,0 +1,100
1 import { TextWriterBase } from "../text/TextWriterBase";
2 import { isNull, isNullOrEmptyString, isPrimitive } from "../safe";
3 import { NullConsole } from "./NullConsole";
4
5 interface LogConsole {
6 debug(...args: any[]): void;
7 log(...args: any[]): void;
8 warn(...args: any[]): void;
9 error(...args: any[]): void;
10 }
11
12 function hasConsole() {
13 try {
14 // tslint:disable-next-line:no-console
15 return (typeof console !== "undefined" && typeof console.log === "function");
16 } catch {
17 return false;
18 }
19 }
20
21 function getConsole() {
22 return hasConsole() ? console : NullConsole.instance;
23 }
24
25 export class ConsoleWriter extends TextWriterBase {
26 static readonly default = new ConsoleWriter(getConsole());
27
28 private _buffer: any[];
29
30 private _out: LogConsole;
31 private _level: keyof LogConsole;
32
33 constructor(out?: LogConsole) {
34 super();
35 this._out = out || NullConsole.instance;
36 this._buffer = [];
37 this._level = "log";
38 }
39
40 getLogLevel() {
41 return this._level;
42 }
43
44 setLogLevel(level: keyof LogConsole) {
45 this._level = level;
46 }
47
48 /** Flushes the buffer to the console
49 */
50 writeNewLine() {
51 // group text chunks together, and let objects as is
52 // ['a', 'b', {foo: 'bar'}, 'c', 'd'] -> ['ab', {foo: 'bar'}, 'cd']
53 // this will prevent from additional spaces to occur in the console
54 // ['a', 'b'] will be printed as 'a b' rather then 'ab'.
55
56 // console.log("writeLine", this._buffer);
57
58 let offset = 0;
59 const args = [];
60 this._buffer.forEach((v, i) => {
61 if (!isPrimitive(v)) {
62 if (offset < i)
63 args.push(i - offset > 1 ? this._buffer.slice(offset, i).join("") : this._buffer[offset]);
64 args.push(v);
65 offset = i + 1;
66 }
67 });
68 if (offset < this._buffer.length)
69 args.push(this._buffer.slice(offset).join(""));
70
71 // console.log("WriteLine", args);
72
73 this._out[this._level].apply(this._out, args);
74
75 this._buffer = [];
76 }
77
78 /** Adds a text chunk to the buffer. Buffer contents will be flushed when
79 * the end of line will be printed.
80 *
81 * @param text The text to be added to the buffer.
82 */
83 writeText(text: string) {
84 this._buffer.push(text);
85 }
86
87 /** Wrotes the specified value to the buffer.
88 *
89 * @param value The value to be added to the buffer
90 * @param spec The instructions how to format the value, is this parameter
91 * is ommited the raw value will be added to the buffer and
92 * passed directly to the console out.
93 */
94 writeValue(value: any, spec?: string) {
95 if (isNullOrEmptyString(spec))
96 this._buffer.push(value);
97 else
98 super.writeValue(value);
99 }
100 }
@@ -0,0 +1,52
1 export class NullConsole {
2 static readonly instance = new NullConsole();
3
4 assert(condition?: boolean, message?: string, ...data: any[]): void {
5 }
6 clear(): void {
7 }
8 count(label?: string): void {
9 }
10 debug(message?: any, ...optionalParams: any[]): void {
11 }
12 dir(value?: any, ...optionalParams: any[]): void {
13 }
14 dirxml(value: any): void {
15 }
16 error(message?: any, ...optionalParams: any[]): void {
17 }
18 exception(message?: string, ...optionalParams: any[]): void {
19 }
20 group(groupTitle?: string, ...optionalParams: any[]): void {
21 }
22 groupCollapsed(groupTitle?: string, ...optionalParams: any[]): void {
23 }
24 groupEnd(): void {
25 }
26 info(message?: any, ...optionalParams: any[]): void {
27 }
28 log(message?: any, ...optionalParams: any[]): void {
29 }
30 markTimeline(label?: string): void {
31 }
32 profile(reportName?: string): void {
33 }
34 profileEnd(reportName?: string): void {
35 }
36 table(...tabularData: any[]): void {
37 }
38 time(label?: string): void {
39 }
40 timeEnd(label?: string): void {
41 }
42 timeStamp(label?: string): void {
43 }
44 timeline(label?: string): void {
45 }
46 timelineEnd(label?: string): void {
47 }
48 trace(message?: any, ...optionalParams: any[]): void {
49 }
50 warn(message?: any, ...optionalParams: any[]): void {
51 }
52 }
@@ -0,0 +1,20
1 import { TraceEvent, TraceSource } from "./TraceSource";
2 import { format } from "../text/StringBuilder";
3
4 export class TraceEventData implements TraceEvent {
5 source: TraceSource;
6 level: number;
7 message: any;
8 args?: any[];
9
10 constructor(source: TraceSource, level: number, message: any, args: any[]) {
11 this.source = source;
12 this.level = level;
13 this.message = message;
14 this.args = args;
15 }
16
17 toString() {
18 return format(this.message, ...this.args);
19 }
20 }
@@ -0,0 +1,41
1 import { IObservable, IDestroyable, ICancellation } from "../../interfaces";
2 import { TraceEvent, LogLevel, WarnLevel, DebugLevel } from "../TraceSource";
3 import { Cancellation } from "../../Cancellation";
4 import { destroy, argumentNotNull } from "../../safe";
5 import { ConsoleWriter } from "../ConsoleWriter";
6
7 export class ConsoleLogger implements IDestroyable {
8 private readonly _subscriptions = new Array<IDestroyable>();
9 private readonly _writer: ConsoleWriter;
10
11 constructor(writer = ConsoleWriter.default) {
12 argumentNotNull(writer, "writer");
13 this._writer = writer;
14 }
15
16 writeEvents(source: IObservable<TraceEvent>, ct: ICancellation = Cancellation.none) {
17 const subscription = source.on(this.writeEvent.bind(this));
18 if (ct.isSupported()) {
19 ct.register(subscription.destroy.bind(subscription));
20 }
21 this._subscriptions.push(subscription);
22 }
23
24 writeEvent(next: TraceEvent) {
25 if (next.level >= DebugLevel) {
26 this._writer.setLogLevel("debug");
27 } else if (next.level >= LogLevel) {
28 this._writer.setLogLevel("log");
29 } else if (next.level >= WarnLevel) {
30 this._writer.setLogLevel("warn");
31 } else {
32 this._writer.setLogLevel("error");
33 }
34 this._writer.write("{0}: ", next.source.id);
35 this._writer.writeLine(next.message, ...next.args);
36 }
37
38 destroy() {
39 this._subscriptions.forEach(destroy);
40 }
41 }
@@ -0,0 +1,30
1 import { isPrimitive, isNull } from "../safe";
2
3 export class Converter {
4 static readonly default = new Converter();
5
6 convert(value: any, pattern: string) {
7 if (pattern && pattern.toLocaleLowerCase() === "json") {
8 const seen = [];
9 return JSON.stringify(value, (k, v) => {
10 if (!isPrimitive(v)) {
11 const id = seen.indexOf(v);
12 if (id >= 0)
13 return "@ref-" + id;
14 else {
15 seen.push(v);
16 return v;
17 }
18 } else {
19 return v;
20 }
21 }, 2);
22 } else if (isNull(value)) {
23 return "";
24 } else if (value instanceof Date) {
25 return value.toISOString();
26 } else {
27 return value.toString();
28 }
29 }
30 }
@@ -0,0 +1,19
1 import { TextWriter } from "../interfaces";
2
3 export class NullTextWriter implements TextWriter {
4 static readonly instance = new NullTextWriter();
5
6 write(obj: any): void;
7 write(format: string, ...args: any[]): void;
8 write() {
9 }
10
11 writeLine(obj: any): void;
12 writeLine(format: string, ...args: any[]): void;
13 writeLine() {
14 }
15
16 writeValue(value: any, spec?: string): void;
17 writeValue() {
18 }
19 }
@@ -0,0 +1,48
1 import { TextWriter } from "../interfaces";
2 import { FormatCompiler } from "./FormatCompiler";
3 import { isString, argumentNotNull } from "../safe";
4 import { Converter } from "./Converter";
5
6 const compiler = new FormatCompiler();
7
8 export abstract class TextWriterBase implements TextWriter {
9 private _converter: Converter;
10
11 constructor(converter = Converter.default) {
12 argumentNotNull(converter, "converter");
13 this._converter = converter;
14 }
15
16 writeNewLine() {
17 this.writeValue("\n");
18 }
19
20 write(obj: any): void;
21 write(format: string, ...args: any[]): void;
22 write(format: any, ...args: any[]): void {
23 if (args.length) {
24 const compiled = compiler.compile(format);
25 compiled(this, args);
26 } else {
27 this.writeValue(format);
28 }
29 }
30
31 writeLine(obj?: any): void;
32 writeLine(format: string, ...args: any[]): void;
33 writeLine(): void {
34 if (arguments.length)
35 this.write.apply(this, arguments);
36 this.writeNewLine();
37 }
38
39 writeValue(value: any, spec?: string): void {
40 this.writeText(
41 isString(value) ?
42 value :
43 this._converter.convert(value, spec)
44 );
45 }
46
47 abstract writeText(text: string);
48 }
@@ -0,0 +1,86
1 import { StringBuilder } from "@implab/core/text/StringBuilder";
2 import { test } from "./TestTraits";
3 import { MockConsole } from "./mock/MockConsole";
4 import { ConsoleWriter } from "@implab/core/log/ConsoleWriter";
5
6 test("String builder", async t => {
7 const sb = new StringBuilder();
8
9 sb.write("hello");
10 t.equals(sb.toString(), "hello", "Write simple text");
11
12 sb.write(", ");
13 sb.write("world!");
14 t.equals(sb.toString(), "hello, world!", "Append text");
15
16 sb.clear();
17 t.equals(sb.toString(), "", "Clear");
18
19 sb.write(1);
20 t.equals(sb.toString(), "1", "Write number");
21
22 sb.clear();
23 sb.writeValue(0.123);
24 t.equals(sb.toString(), "0.123", "Format number");
25
26 sb.clear();
27 sb.writeValue(new Date("2019-01-02T00:00:00.000Z"));
28 t.equals(sb.toString(), "2019-01-02T00:00:00.000Z", "Format date (ISO)");
29
30 sb.clear();
31 sb.write("{0}", "hello");
32 t.equals(sb.toString(), "hello", "Simple format text");
33
34 sb.write(", {0}!", "world");
35 t.equals(sb.toString(), "hello, world!", "Append formatted text");
36
37 sb.clear();
38 sb.write("abc: {0:json}; {0.length}; {0.1} {{olo}}", ["a", "b", "c"]);
39 t.equals(sb.toString(), 'abc: [\n "a",\n "b",\n "c"\n]; 3; b {olo}', "Format string with spec");
40
41 sb.clear();
42 t.throws(() => sb.write("}", 0), "Should die on bad format: '}'");
43 t.throws(() => sb.write("{", 0), "Should die on bad format: '{'");
44 t.throws(() => sb.write("{}", 0), "Should die on bad format: '{}'");
45 t.throws(() => sb.write("{:}", 0), "Should die on bad format: '{:}'");
46 t.throws(() => sb.write("{{0}", 0), "Should die on bad format: '{{0}'");
47
48 });
49
50 test("ConsoleWriter", t => {
51 const mockConsole = new MockConsole();
52 const writer = new ConsoleWriter(mockConsole);
53
54 writer.setLogLevel("log");
55
56 writer.writeLine("Hello, world!");
57
58 t.equals(mockConsole.getBuffer().length, 1, "One line should be written");
59 t.equals(mockConsole.getBuffer()[0].level, "log", "LogLevel should be 'log'");
60 t.deepEqual(mockConsole.getBuffer()[0].data, ["Hello, world!"], "The buffer should contain single string");
61
62 mockConsole.clear();
63 writer.setLogLevel("debug");
64 writer.write("Bring ");
65 writer.write("the {0}!", "light");
66 t.equals(mockConsole.getBuffer().length, 0, "No line should be written");
67 writer.writeLine();
68
69 t.equals(mockConsole.getBuffer().length, 1, "One line should be written");
70 t.equals(mockConsole.getBuffer()[0].level, "debug", "LogLevel should be 'log'");
71 t.deepEqual(mockConsole.getBuffer()[0].data, ["Bring the light!"], "Should concatenate string parts together");
72
73 mockConsole.clear();
74 writer.writeLine("It's {0} o'clock, lets have some {1}!", { h: 5}, { title: "tee" });
75
76 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");
77
78 mockConsole.clear();
79 writer.writeLine("{0} or {1} to {2}", {i: 25}, 6, 4);
80 t.deepEqual(mockConsole.getBuffer()[0].data, [{i: 25}, " or 6 to 4"], "25 or 6 to 4");
81
82 mockConsole.clear();
83 writer.writeLine("{0} or {1} to {2}! Let's have some {3}", 25, 6, 4, { product: "tee" } );
84 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");
85
86 });
@@ -0,0 +1,52
1 import {NullConsole} from "@implab/core/log/NullConsole";
2
3 interface ConsoleLineData {
4 level: string;
5 data: any[];
6 }
7
8 export class MockConsole extends NullConsole {
9 _buffer: ConsoleLineData[] = [];
10
11 debug(...args: any[]) {
12 this._buffer.push({
13 level: "debug",
14 data: args
15 });
16 }
17
18 log(...args: any[]) {
19 this._buffer.push({
20 level: "log",
21 data: args
22 });
23 }
24
25 warn(...args: any[]) {
26 this._buffer.push({
27 level: "warn",
28 data: args
29 });
30 }
31
32 error(...args: any[]) {
33 this._buffer.push({
34 level: "error",
35 data: args
36 });
37 }
38
39 getBuffer() {
40 return this._buffer;
41 }
42
43 getLine(i: number) {
44 if (i >= this._buffer.length)
45 throw new Error(`Line number ${i} is out of range, buffer.length = ${this._buffer.length}`);
46 return this._buffer[i].data;
47 }
48
49 clear() {
50 this._buffer = [];
51 }
52 }
@@ -99,6 +99,8 def createSoursetTasks = { String name,
99 99 '-t', target,
100 100 '-m', jsmodule,
101 101 '-d',
102 '--sourceMap',
103 '--sourceRoot', "file://$setDir/ts",
102 104 '--outDir', compileDir,
103 105 '--declarationDir', declDir
104 106
@@ -6,4 +6,4 description=Dependency injection, loggin
6 6 license=BSD-2-Clause
7 7 repository=https://bitbucket.org/implab/implabjs-core
8 8 npmScope=implab
9 npmName=core No newline at end of file
9 npmName=core-amd No newline at end of file
@@ -90,7 +90,7
90 90 },
91 91 "duplexer": {
92 92 "version": "0.1.1",
93 "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
93 "resolved": "http://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
94 94 "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=",
95 95 "dev": true
96 96 },
@@ -144,7 +144,7
144 144 "dependencies": {
145 145 "tape": {
146 146 "version": "2.3.3",
147 "resolved": "https://registry.npmjs.org/tape/-/tape-2.3.3.tgz",
147 "resolved": "http://registry.npmjs.org/tape/-/tape-2.3.3.tgz",
148 148 "integrity": "sha1-Lnzgox3wn41oUWZKcYQuDKUFevc=",
149 149 "dev": true,
150 150 "requires": {
@@ -277,7 +277,7
277 277 },
278 278 "minimist": {
279 279 "version": "0.0.5",
280 "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz",
280 "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz",
281 281 "integrity": "sha1-16oye87PUY+RBqxrjwA/o7zqhWY=",
282 282 "dev": true
283 283 },
@@ -316,7 +316,7
316 316 },
317 317 "readable-stream": {
318 318 "version": "1.1.14",
319 "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
319 "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
320 320 "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
321 321 "dev": true,
322 322 "requires": {
@@ -369,7 +369,7
369 369 },
370 370 "string_decoder": {
371 371 "version": "0.10.31",
372 "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
372 "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
373 373 "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
374 374 "dev": true
375 375 },
@@ -432,7 +432,7
432 432 },
433 433 "through2": {
434 434 "version": "0.2.3",
435 "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz",
435 "resolved": "http://registry.npmjs.org/through2/-/through2-0.2.3.tgz",
436 436 "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=",
437 437 "dev": true,
438 438 "requires": {
@@ -7,6 +7,23 const trace = TraceSource.get(m.id);
7 7
8 8 type TemplateFn = (obj: object) => string;
9 9
10 const htmlEscapes = {
11 "&": "&amp;",
12 "<": "&lt;",
13 ">": "&gt;",
14 '"': "&quot;",
15 "'": "&#x27;",
16 "/": "&#x2F;"
17 };
18
19 // Regex containing the keys listed immediately above.
20 const htmlEscaper = /[&<>"'\/]/g;
21
22 // Escape a string for HTML interpolation.
23 function escapeHtml(string: any) {
24 return ("" + string).replace(htmlEscaper, match => htmlEscapes[match]);
25 }
26
10 27 export class TemplateCompiler {
11 28
12 29 _data: string[];
@@ -27,14 +44,14 export class TemplateCompiler {
27 44
28 45 try {
29 46 // tslint:disable-next-line:function-constructor
30 const compiled = new Function("obj, format, $data", text);
47 const compiled = new Function("obj, format, $data, escapeHtml", text);
31 48 /**
32 49 * Функция форматирования по шаблону
33 50 *
34 51 * @type{Function}
35 52 * @param{Object} obj объект с параметрами для подстановки
36 53 */
37 return (obj: object) => compiled(obj || {}, format, this._data);
54 return (obj: object) => compiled(obj || {}, format, this._data, escapeHtml);
38 55 } catch (e) {
39 56 trace.traceEvent(DebugLevel, [e, text, this._data]);
40 57 throw e;
@@ -69,6 +86,9 export class TemplateCompiler {
69 86 case TokenType.OpenInlineBlock:
70 87 this.visitInline(parser);
71 88 break;
89 case TokenType.OpenFilterBlock:
90 this.visitFilter(parser);
91 break;
72 92 default:
73 93 this.visitTextFragment(parser);
74 94 break;
@@ -87,6 +107,17 export class TemplateCompiler {
87 107 this._code.push(code.join(""));
88 108 }
89 109
110 visitFilter(parser: ITemplateParser) {
111 const code = ["$p.push(escapeHtml("];
112 while (parser.next()) {
113 if (parser.token() === TokenType.CloseBlock)
114 break;
115 code.push(parser.value());
116 }
117 code.push("));");
118 this._code.push(code.join(""));
119 }
120
90 121 visitCode(parser: ITemplateParser) {
91 122 const code = [];
92 123 while (parser.next()) {
@@ -5,12 +5,13 import m = require("module");
5 5
6 6 const trace = TraceSource.get(m.id);
7 7
8 const splitRx = /(<%=|\[%=|<%|\[%|%\]|%>)/;
8 const splitRx = /(<%=|<%~|\[%~|\[%=|<%|\[%|%\]|%>)/;
9 9
10 10 export enum TokenType {
11 11 None,
12 12 Text,
13 13 OpenInlineBlock,
14 OpenFilterBlock,
14 15 OpenBlock,
15 16 CloseBlock
16 17 }
@@ -20,6 +21,8 const tokenMap: MapOf<TokenType> = {
20 21 "[%": TokenType.OpenBlock,
21 22 "<%=": TokenType.OpenInlineBlock,
22 23 "[%=": TokenType.OpenInlineBlock,
24 "<%~": TokenType.OpenFilterBlock,
25 "[%~": TokenType.OpenFilterBlock,
23 26 "%>": TokenType.CloseBlock,
24 27 "%]": TokenType.CloseBlock
25 28 };
@@ -101,11 +101,11 export interface IObserver<T> {
101 101 }
102 102
103 103 export interface TextWriter {
104 Write(obj: any): void;
105 Write(format: string, ...args: any[]): void;
104 write(obj: any): void;
105 write(format: string, ...args: any[]): void;
106 106
107 WriteLine(obj: any): void;
108 WriteLine(format: string, ...args: any[]): void;
107 writeLine(obj?: any): void;
108 writeLine(format: string, ...args: any[]): void;
109 109
110 WriteValue(value: any, spec?: string): void;
110 writeValue(value: any, spec?: string): void;
111 111 }
@@ -1,6 +1,6
1 1 import { Observable } from "../Observable";
2 2 import { Registry } from "./Registry";
3 import { format as _format } from "../text/StringFormat";
3 import { TraceEventData } from "./TraceEventData";
4 4
5 5 export const DebugLevel = 400;
6 6
@@ -17,13 +17,9 export interface TraceEvent {
17 17
18 18 readonly level: number;
19 19
20 readonly arg: any;
21 }
20 readonly message: any;
22 21
23 function format(msg) {
24 if (typeof(msg) !== "string" || arguments.length === 1)
25 return msg;
26 return _format.apply(null, arguments);
22 readonly args?: any[];
27 23 }
28 24
29 25 export class TraceSource {
@@ -43,35 +39,41 export class TraceSource {
43 39 });
44 40 }
45 41
46 protected emit(level: number, arg: any) {
47 this._notifyNext({ source: this, level, arg });
42 protected emit(level: number, message: any, args?: any[]) {
43 this._notifyNext(new TraceEventData(this, level, message, args));
48 44 }
49 45
50 46 isDebugEnabled() {
51 47 return this.level >= DebugLevel;
52 48 }
53 49
50 debug(data: any): void;
51 debug(msg: string, ...args: any[]): void;
54 52 debug(msg: string, ...args: any[]) {
55 53 if (this.isEnabled(DebugLevel))
56 this.emit(DebugLevel, format.apply(null, arguments));
54 this.emit(DebugLevel, msg, args);
57 55 }
58 56
59 57 isLogEnabled() {
60 58 return this.level >= LogLevel;
61 59 }
62 60
61 log(data: any): void;
62 log(msg: string, ...args: any[]): void;
63 63 log(msg: string, ...args: any[]) {
64 64 if (this.isEnabled(LogLevel))
65 this.emit(LogLevel, format.apply(null, arguments));
65 this.emit(LogLevel, msg, args);
66 66 }
67 67
68 68 isWarnEnabled() {
69 69 return this.level >= WarnLevel;
70 70 }
71 71
72 warn(data: any): void;
73 warn(msg: string, ...args: any[]): void;
72 74 warn(msg: string, ...args: any[]) {
73 75 if (this.isEnabled(WarnLevel))
74 this.emit(WarnLevel, format.apply(null, arguments));
76 this.emit(WarnLevel, msg, args);
75 77 }
76 78
77 79 /**
@@ -81,15 +83,20 export class TraceSource {
81 83 return this.level >= ErrorLevel;
82 84 }
83 85
86 /** Traces a error
87 * @param data The object which will be passed to the underlying listeners
88 */
89 error(data: any): void;
84 90 /**
85 91 * Traces a error.
86 92 *
87 93 * @param msg the message.
88 94 * @param args parameters which will be substituted in the message.
89 95 */
96 error(msg: string, ...args: any[]): void;
90 97 error(msg: string, ...args: any[]) {
91 98 if (this.isEnabled(ErrorLevel))
92 this.emit(ErrorLevel, format.apply(null, arguments));
99 this.emit(ErrorLevel, msg, args);
93 100 }
94 101
95 102 /**
@@ -106,11 +113,11 export class TraceSource {
106 113 * Traces a raw event, passing data as it is to the underlying listeners
107 114 *
108 115 * @param level the level of the event
109 * @param arg the data of the event, can be a simple string or any object.
116 * @param msg the data of the event, can be a simple string or any object.
110 117 */
111 traceEvent(level: number, arg: any) {
118 traceEvent(level: number, msg: any, ...args: any[]) {
112 119 if (this.isEnabled(level))
113 this.emit(level, arg);
120 this.emit(level, msg, args);
114 121 }
115 122
116 123 /**
@@ -1,49 +1,1
1 import { IObservable, IDestroyable, ICancellation } from "../../interfaces";
2 import { TraceEvent, LogLevel, WarnLevel, DebugLevel } from "../TraceSource";
3 import { Cancellation } from "../../Cancellation";
4 import { destroy } from "../../safe";
5
6 function hasConsole() {
7 try {
8 // tslint:disable-next-line:no-console
9 return (typeof console !== "undefined" && typeof console.log === "function");
10 } catch {
11 return false;
12 }
13 }
14
15 export class ConsoleWriter implements IDestroyable {
16 readonly _subscriptions = new Array<IDestroyable>();
17
18 writeEvents(source: IObservable<TraceEvent>, ct: ICancellation = Cancellation.none) {
19 const subscription = source.on(this.writeEvent.bind(this));
20 if (ct.isSupported()) {
21 ct.register(subscription.destroy.bind(subscription));
22 }
23 this._subscriptions.push(subscription);
24 }
25
26 writeEvent(next: TraceEvent) {
27 // IE will create console only when devepoler tools are activated
28 if (!hasConsole())
29 return;
30
31 if (next.level >= DebugLevel) {
32 // tslint:disable-next-line:no-console
33 console.debug(next.source.id.toString(), next.arg);
34 } else if (next.level >= LogLevel) {
35 // tslint:disable-next-line:no-console
36 console.log(next.source.id.toString(), next.arg);
37 } else if (next.level >= WarnLevel) {
38 // tslint:disable-next-line:no-console
39 console.warn(next.source.id.toString(), next.arg);
40 } else {
41 // tslint:disable-next-line:no-console
42 console.error(next.source.id.toString(), next.arg);
43 }
44 }
45
46 destroy() {
47 this._subscriptions.forEach(destroy);
48 }
49 }
1 export { ConsoleLogger as ConsoleWriter } from "./ConsoleLogger"; No newline at end of file
@@ -1,20 +1,41
1 1 import { FormatScanner, TokeType } from "./FormatScanner";
2 import { isNullOrEmptyString } from "../safe";
3 import { TextWriter } from "../interfaces";
2 import { isNullOrEmptyString, isPrimitive, get } from "../safe";
3 import { TextWriter, MapOf } from "../interfaces";
4
5 type CompiledPattern = (writer: TextWriter, args: any) => void;
4 6
5 7 export class FormatCompiler {
6 8 _scanner: FormatScanner;
9 _cache: MapOf<CompiledPattern> = {};
7 10
8 _parts: [];
11 _parts: Array<string | { name: string; format: string; }>;
12
13 compile(pattern: string) {
14 let compiledPattern = this._cache && this._cache[pattern];
15 if (!compiledPattern) {
16 this._scanner = new FormatScanner(pattern);
17 this._parts = [];
18
19 this.visitText();
20 const parts = this._parts;
9 21
10 compile() {
11 return (writer: TextWriter, args: any) => {
12 this._parts.forEach(x => writer.WriteValue(x))
13 };
22 compiledPattern = (writer: TextWriter, args: any) => {
23 parts.forEach(x => {
24 if (isPrimitive(x))
25 writer.writeValue(x);
26 else
27 writer.writeValue(get(x.name, args), x.format);
28 });
29 };
30 if (this._cache)
31 this._cache[pattern] = compiledPattern;
32 }
33 return compiledPattern;
14 34 }
15 35
16 36 visitText() {
17 37 while (this._scanner.next()) {
38 // console.log(this._scanner.getTokenType(), this._scanner.getTokenValue());
18 39 switch (this._scanner.getTokenType()) {
19 40 case TokeType.CurlOpen:
20 41 this.visitCurlOpen();
@@ -25,8 +46,6 export class FormatCompiler {
25 46 default:
26 47 this.pushText(this._scanner.getTokenValue());
27 48 }
28 if (this._scanner.getTokenType() === TokeType.CurlOpen)
29 this.visitCurlOpen();
30 49 }
31 50 }
32 51
@@ -39,12 +58,14 export class FormatCompiler {
39 58 }
40 59
41 60 visitCurlOpen() {
42 if (this._scanner.next()) {
43 if (this._scanner.getTokenType() === TokeType.CurlOpen)
44 this.pushText("{");
45 else
46 this.visitTemplateSubst();
47 }
61 if (!this._scanner.next())
62 this.dieUnexpectedEnd("{ | TEXT");
63
64 if (this._scanner.getTokenType() === TokeType.CurlOpen)
65 this.pushText("{");
66 else
67 this.visitTemplateSubst();
68
48 69 }
49 70
50 71 visitTemplateSubst() {
@@ -52,20 +73,23 export class FormatCompiler {
52 73 this.dieUnexpectedToken("TEXT");
53 74
54 75 const fieldName = this._scanner.getTokenValue();
55 const filedFormat = this.readColon() && this.readFieldFormat();
76 const filedFormat = this.readColon() ? this.readFieldFormat() : null;
77
78 if (this._scanner.getTokenType() !== TokeType.CurlClose)
79 this.dieUnexpectedToken("}");
56 80
57 81 this.pushSubst(fieldName, filedFormat);
58 82 }
59 83
60 84 readFieldFormat() {
61 85 const parts = new Array<string>();
62 while (this._scanner.next()) {
86 do {
63 87 if (this._scanner.getTokenType() === TokeType.CurlClose) {
64 88 return parts.join("");
65 89 } else {
66 90 parts.push(this._scanner.getTokenValue());
67 91 }
68 }
92 } while (this._scanner.next());
69 93
70 94 this.dieUnexpectedEnd("}");
71 95 }
@@ -81,11 +105,12 export class FormatCompiler {
81 105 }
82 106
83 107 pushSubst(fieldName: string, filedFormat: string) {
84
108 // console.log("pushSubst ", fieldName, filedFormat);
109 this._parts.push({ name: fieldName, format: filedFormat });
85 110 }
86 111
87 112 pushText(text: string) {
88
113 this._parts.push(text);
89 114 }
90 115
91 116 dieUnexpectedToken(expected?: string) {
@@ -2,10 +2,10 import { argumentNotEmptyString } from "
2 2 import { MapOf } from "../interfaces";
3 3
4 4 export const enum TokeType {
5 CurlOpen,
6 CurlClose,
7 Colon,
8 Text
5 CurlOpen = 1,
6 CurlClose = 2,
7 Colon = 3,
8 Text = 4
9 9 }
10 10
11 11 const typeMap = {
@@ -16,7 +16,6 const typeMap = {
16 16
17 17 export class FormatScanner {
18 18 private _text: string;
19 private _pos: number;
20 19 private _tokenType: TokeType;
21 20 private _tokenValue: string;
22 21 private _rx = /[^{}:]+|(.)/g;
@@ -29,7 +28,6 export class FormatScanner {
29 28 next() {
30 29 if (this._rx.lastIndex >= this._text.length)
31 30 return false;
32 this._pos = this._rx.lastIndex;
33 31
34 32 const match = this._rx.exec(this._text);
35 33 this._tokenType = typeMap[match[1]] || TokeType.Text;
@@ -1,18 +1,31
1 export class StringBuilder {
2 private _data: string[];
3 private _newLine = "\n";
1 import { TextWriterBase } from "./TextWriterBase";
2 import { Converter } from "./Converter";
3
4 export class StringBuilder extends TextWriterBase {
5 private _data = new Array<string>();
4 6
5 Write(obj: any);
6 Write(format: string, ...args: any[]) {
7 constructor(converter = Converter.default) {
8 super(converter);
9 }
7 10
11 writeText(text: string) {
12 this._data.push(text);
8 13 }
9 14
10 WriteLine(obj: any);
11 WriteLine(format: string, ...args: any[]) {
12
15 toString() {
16 return this._data.join("");
13 17 }
14 18
15 WriteValue(value: any, spec?: string) {
16
19 clear() {
20 this._data.length = 0;
17 21 }
18 22 }
23
24 const sb = new StringBuilder();
25
26 export function format(format: string, ...args: any): string;
27 export function format() {
28 sb.clear();
29 sb.write.apply(sb, arguments);
30 return sb.toString();
31 }
@@ -1,8 +1,8
1 import * as tape from "tape";
2 1 import { MockActivationController } from "./mock/MockActivationController";
3 2 import { SimpleActivatable } from "./mock/SimpleActivatable";
3 import { test } from "./TestTraits";
4 4
5 tape("simple activation", async t => {
5 test("simple activation", async t => {
6 6
7 7 const a = new SimpleActivatable();
8 8 t.false(a.isActive());
@@ -12,11 +12,9 tape("simple activation", async t => {
12 12
13 13 await a.deactivate();
14 14 t.false(a.isActive());
15
16 t.end();
17 15 });
18 16
19 tape("controller activation", async t => {
17 test("controller activation", async t => {
20 18
21 19 const a = new SimpleActivatable();
22 20 const c = new MockActivationController();
@@ -37,11 +35,9 tape("controller activation", async t =>
37 35 t.false(a.isActive(), "The component should successfully deactivate");
38 36 t.equal(c.getActive(), null, "The controller shouldn't point to any component");
39 37 t.equal(a.getActivationController(), c, "The componet should point to it's controller");
40
41 t.end();
42 38 });
43 39
44 tape("handle error in onActivating", async t => {
40 test("handle error in onActivating", async t => {
45 41 const a = new SimpleActivatable();
46 42
47 43 a.onActivating = async () => {
@@ -55,6 +51,4 tape("handle error in onActivating", asy
55 51 }
56 52
57 53 t.false(a.isActive(), "the component should remain inactive");
58
59 t.end();
60 54 });
@@ -1,8 +1,8
1 import * as tape from "tape";
2 1 import { Cancellation } from "@implab/core/Cancellation";
3 2 import { delay } from "@implab/core/safe";
3 import { test } from "./TestTraits";
4 4
5 tape("standalone cancellation", async t => {
5 test("standalone cancellation", async t => {
6 6
7 7 let doCancel: (e) => void;
8 8
@@ -43,11 +43,9 tape("standalone cancellation", async t
43 43 msg = e;
44 44 }
45 45 t.equals(msg, reason, "The cancellation reason should be catched");
46
47 t.end();
48 46 });
49 47
50 tape("async cancellation", async t => {
48 test("async cancellation", async t => {
51 49
52 50 const ct = new Cancellation(cancel => {
53 51 cancel("STOP!");
@@ -59,11 +57,9 tape("async cancellation", async t => {
59 57 } catch (e) {
60 58 t.equals(e, "STOP!", "Should throw the cancellation reason");
61 59 }
62
63 t.end();
64 60 });
65 61
66 tape("cancel with external event", async t => {
62 test("cancel with external event", async t => {
67 63 const ct = new Cancellation(cancel => {
68 64 setTimeout(x => cancel("STOP!"), 0);
69 65 });
@@ -74,11 +70,9 tape("cancel with external event", async
74 70 } catch (e) {
75 71 t.equals(e, "STOP!", "Should throw the cancellation reason");
76 72 }
77
78 t.end();
79 73 });
80 74
81 tape("operation normal flow", async t => {
75 test("operation normal flow", async t => {
82 76
83 77 let htimeout;
84 78 const ct = new Cancellation(cancel => {
@@ -91,6 +85,4 tape("operation normal flow", async t =>
91 85 } finally {
92 86 clearTimeout(htimeout);
93 87 }
94
95 t.end();
96 88 });
@@ -1,12 +1,12
1 1 import { TraceSource, DebugLevel } from "@implab/core/log/TraceSource";
2 import * as tape from "tape";
3 2 import { Observable } from "@implab/core/Observable";
4 3 import { IObservable } from "@implab/core/interfaces";
5 4 import { delay } from "@implab/core/safe";
5 import { test } from "./TestTraits";
6 6
7 7 const trace = TraceSource.get("ObservableTests");
8 8
9 tape("events sequence example", async t => {
9 test("events sequence example", async t => {
10 10
11 11 let events: IObservable<number>;
12 12
@@ -34,11 +34,9 tape("events sequence example", async t
34 34
35 35 t.equals(count, 45, "the summ of the evetns");
36 36 t.true(complete, "the sequence is complete");
37
38 t.end();
39 37 });
40 38
41 tape("event sequence termination", async t => {
39 test("event sequence termination", async t => {
42 40 let events: IObservable<number>;
43 41
44 42 const done = new Promise<void>(resolve => {
@@ -68,6 +66,4 tape("event sequence termination", async
68 66 await done;
69 67
70 68 t.equals(count, 1, "the sequence must be terminated once");
71
72 t.end();
73 69 });
@@ -1,8 +1,8
1 import tape = require("tape");
2 1 import { Cancellation } from "@implab/core/Cancellation";
3 2 import { first, isPromise, firstWhere, delay, nowait } from "@implab/core/safe";
3 import { test } from "./TestTraits";
4 4
5 tape("await delay test", async t => {
5 test("await delay test", async t => {
6 6 // schedule delay
7 7 let resolved = false;
8 8 let res = delay(0).then(() => resolved = true);
@@ -36,11 +36,9 tape("await delay test", async t => {
36 36 // try schedule delay after the cancellation is requested
37 37 nowait(delay(0, ct));
38 38 }, "Should throw if cancelled before start");
39
40 t.end();
41 39 });
42 40
43 tape("sequemce test", async t => {
41 test("sequemce test", async t => {
44 42 const sequence = ["a", "b", "c"];
45 43 const empty = [];
46 44
@@ -94,6 +92,4 tape("sequemce test", async t => {
94 92
95 93 v = await new Promise(resolve => first(asyncSequence, resolve));
96 94 t.equal(v, "a", "The callback should be called for the first element");
97
98 t.end();
99 95 });
@@ -5,9 +5,10 import * as tape from "tape";
5 5 import { argumentNotNull, destroy } from "@implab/core/safe";
6 6
7 7 export class TapeWriter implements IDestroyable {
8 readonly _tape: tape.Test;
8 private readonly _tape: tape.Test;
9 9
10 _subscriptions = new Array<IDestroyable>();
10 private readonly _subscriptions = new Array<IDestroyable>();
11 private _destroyed;
11 12
12 13 constructor(t: tape.Test) {
13 14 argumentNotNull(t, "tape");
@@ -15,22 +16,24 export class TapeWriter implements IDest
15 16 }
16 17
17 18 writeEvents(source: IObservable<TraceEvent>, ct: ICancellation = Cancellation.none) {
18 const subscription = source.on(this.writeEvent.bind(this));
19 if (ct.isSupported()) {
20 ct.register(subscription.destroy.bind(subscription));
19 if (!this._destroyed) {
20 const subscription = source.on(this.writeEvent.bind(this));
21 if (ct.isSupported()) {
22 ct.register(subscription.destroy.bind(subscription));
23 }
24 this._subscriptions.push(subscription);
21 25 }
22 this._subscriptions.push(subscription);
23 26 }
24 27
25 28 writeEvent(next: TraceEvent) {
26 29 if (next.level >= DebugLevel) {
27 this._tape.comment(`DEBUG ${next.source.id} ${next.arg}`);
30 this._tape.comment(`DEBUG ${next.source.id} ${next}`);
28 31 } else if (next.level >= LogLevel) {
29 this._tape.comment(`LOG ${next.source.id} ${next.arg}`);
32 this._tape.comment(`LOG ${next.source.id} ${next}`);
30 33 } else if (next.level >= WarnLevel) {
31 this._tape.comment(`WARN ${next.source.id} ${next.arg}`);
34 this._tape.comment(`WARN ${next.source.id} ${next}`);
32 35 } else {
33 this._tape.comment(`ERROR ${next.source.id} ${next.arg}`);
36 this._tape.comment(`ERROR ${next.source.id} ${next}`);
34 37 }
35 38 }
36 39
@@ -39,17 +42,22 export class TapeWriter implements IDest
39 42 }
40 43 }
41 44
42 export function test(name: string, cb: (t: tape.Test) => any) {
45 export function test(name: string, cb: (t: tape.Test, trace: TraceSource) => any) {
43 46 tape(name, async t => {
44 47 const writer = new TapeWriter(t);
45 48
46 TraceSource.on(ts => {
49 // this trace is not announced through the TraceSource global registry
50 const trace = new TraceSource(name);
51 trace.level = DebugLevel;
52 writer.writeEvents(trace.events);
53
54 const h = TraceSource.on(ts => {
47 55 ts.level = DebugLevel;
48 56 writer.writeEvents(ts.events);
49 57 });
50 58
51 59 try {
52 await cb(t);
60 await cb(t, trace);
53 61 } catch (e) {
54 62
55 63 // verbose error information
@@ -60,6 +68,7 export function test(name: string, cb: (
60 68 } finally {
61 69 t.end();
62 70 destroy(writer);
71 destroy(h);
63 72 }
64 73 });
65 74 }
@@ -1,6 +1,9
1 1 import { TraceSource, DebugLevel } from "@implab/core/log/TraceSource";
2 2 import * as tape from "tape";
3 import { TapeWriter } from "./TestTraits";
3 import { TapeWriter, test } from "./TestTraits";
4 import { MockConsole } from "./mock/MockConsole";
5 import { ConsoleLog } from "@implab/core/log/writers/ConsoleLog";
6 import { ConsoleWriter } from "@implab/core/log/ConsoleWriter";
4 7
5 8 const sourceId = "test/TraceSourceTests";
6 9
@@ -12,7 +15,7 tape("trace message", t => {
12 15 const h = trace.events.on(ev => {
13 16 t.equal(ev.source, trace, "sender should be the current trace source");
14 17 t.equal(ev.level, DebugLevel, "level should be debug level");
15 t.equal(ev.arg, "Hello, World!", "The message should be a formatted message");
18 t.equal(ev.toString(), "Hello, World!", "The message should be a formatted message");
16 19
17 20 t.end();
18 21 });
@@ -34,7 +37,7 tape("trace event", t => {
34 37 const h = trace.events.on(ev => {
35 38 t.equal(ev.source, trace, "sender should be the current trace source");
36 39 t.equal(ev.level, DebugLevel, "level should be debug level");
37 t.equal(ev.arg, event, "The message should be the specified object");
40 t.equal(ev.message, event, "The message should be the specified object");
38 41
39 42 t.end();
40 43 });
@@ -67,3 +70,17 tape("tape comment writer", async t => {
67 70
68 71 t.end();
69 72 });
73
74 test("console writer", (t, trace) => {
75
76 const mockConsole = new MockConsole();
77 const writer = new ConsoleWriter(mockConsole);
78 const consoleLog = new ConsoleLog(writer);
79 consoleLog.writeEvents(trace.events);
80
81 trace.log("Hello, world!");
82 t.deepEqual(mockConsole.getLine(0), ["console writer: Hello, world!"], "Log one string");
83
84 trace.log({ foo: "bar" });
85 t.deepEqual(mockConsole.getLine(1), ["console writer: ", { foo: "bar" }], "Log an object");
86 });
@@ -5,5 +5,6 define([
5 5 "./CancellationTests",
6 6 "./ObservableTests",
7 7 "./ContainerTests",
8 "./SafeTests"
8 "./SafeTests",
9 "./TextTests"
9 10 ]); No newline at end of file
@@ -3,3 +3,5 import "./TraceSourceTests";
3 3 import "./CancellationTests";
4 4 import "./ObservableTests";
5 5 import "./ContainerTests";
6 import "./SafeTests";
7 import "./TextTests";
General Comments 0
You need to be logged in to leave comments. Login now