Rename kotlin-test-nodejs-runner to kotlin-test-js-runner
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
import {CliArgValues} from "./CliArgsParser";
|
||||
import {IgnoredTestSuitesReporting, runWithTeamCityReporter} from "./KotlinTestTeamCityReporter";
|
||||
import {TeamCityMessagesFlow} from "./TeamCityMessagesFlow";
|
||||
import {KotlinTestRunner} from "./KotlinTestRunner";
|
||||
import {hrTimer} from "./Timer";
|
||||
import {configureFiltering} from "./CliFiltertingConfiguration";
|
||||
|
||||
export function getAdapter(
|
||||
initialAdapter: KotlinTestRunner,
|
||||
cliArgsValue: CliArgValues
|
||||
): KotlinTestRunner {
|
||||
const args = {
|
||||
onIgnoredTestSuites: (cliArgsValue.ignoredTestSuites
|
||||
|| IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored) as IgnoredTestSuitesReporting,
|
||||
include: cliArgsValue.include as string[],
|
||||
exclude: cliArgsValue.exclude as string[],
|
||||
};
|
||||
|
||||
const teamCity = new TeamCityMessagesFlow(null, (payload) => console.log(payload));
|
||||
|
||||
let runner: KotlinTestRunner = initialAdapter;
|
||||
runner = runWithTeamCityReporter(runner, args.onIgnoredTestSuites, teamCity, hrTimer);
|
||||
runner = configureFiltering(runner, args.include, args.exclude);
|
||||
|
||||
return runner;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import {println, startsWith} from "./utils";
|
||||
import {IgnoredTestSuitesReporting} from "./KotlinTestTeamCityReporter";
|
||||
|
||||
export type CliDescription = {
|
||||
version: string,
|
||||
bin: string,
|
||||
description: string,
|
||||
usage: string,
|
||||
args: {
|
||||
[k: string]: CliArgDescription,
|
||||
},
|
||||
freeArgsTitle: string | null
|
||||
}
|
||||
|
||||
export type CliArgValues = {
|
||||
[k: string]: string[] | string,
|
||||
free: string[]
|
||||
}
|
||||
|
||||
export type CliArgDescription = {
|
||||
keys: string[],
|
||||
help: string,
|
||||
values?: string[],
|
||||
valuesHelp?: string[],
|
||||
default?: string,
|
||||
single?: true
|
||||
}
|
||||
|
||||
export class CliArgsParser {
|
||||
constructor(private description: CliDescription) {
|
||||
}
|
||||
|
||||
printUsage() {
|
||||
const description = this.description;
|
||||
|
||||
println(`${description.bin} v${description.version} - ${description.description}`);
|
||||
println();
|
||||
println(`Usage: ${description.bin} ${description.usage}`);
|
||||
println();
|
||||
for (let key in description.args) {
|
||||
const data = description.args[key];
|
||||
println(' ' + data.keys.join(', '));
|
||||
const indent = ' ';
|
||||
println(`${indent}${data.help}`);
|
||||
if (data.values && data.valuesHelp) {
|
||||
println(`${indent}Possible values:`);
|
||||
for (let i = 0; i < data.values.length; i++) {
|
||||
const value = data.values[i];
|
||||
const help = data.valuesHelp[i];
|
||||
println(`${indent} - "${value}": ${help}`)
|
||||
}
|
||||
}
|
||||
if (data.default) println(`${indent}By default: ${data.default}`);
|
||||
println('')
|
||||
}
|
||||
}
|
||||
|
||||
badArgsExit(message: string) {
|
||||
println(message);
|
||||
println();
|
||||
this.printUsage();
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
parse(args: string[]): CliArgValues {
|
||||
const description = this.description;
|
||||
|
||||
const result: CliArgValues = {
|
||||
free: []
|
||||
};
|
||||
for (let key in description.args) {
|
||||
if (!description.args[key].single) {
|
||||
result[key] = [];
|
||||
}
|
||||
}
|
||||
|
||||
// process all arguments from left to right
|
||||
args: while (args.length != 0) {
|
||||
const arg = args.shift() as string;
|
||||
|
||||
if (startsWith(arg, '--')) {
|
||||
for (let argName in description.args) {
|
||||
const argDescription = description.args[argName];
|
||||
if (argDescription.keys.indexOf(arg) != -1) {
|
||||
if (args.length == 0) {
|
||||
this.badArgsExit("Missed value after option " + arg);
|
||||
}
|
||||
|
||||
const value = args.shift() as string;
|
||||
if (argDescription.values && argDescription.values.indexOf(value) == -1) {
|
||||
this.badArgsExit("Unsupported value for option " + arg);
|
||||
}
|
||||
|
||||
if (argDescription.single) {
|
||||
result[argName] = value;
|
||||
} else {
|
||||
(result[argName] as string[]).push(value);
|
||||
}
|
||||
|
||||
continue args;
|
||||
}
|
||||
}
|
||||
|
||||
this.badArgsExit("Unknown option: " + arg);
|
||||
} else {
|
||||
result.free.push(arg)
|
||||
}
|
||||
}
|
||||
|
||||
if (description.freeArgsTitle && result.free.length == 0) {
|
||||
this.badArgsExit(`At least one ${description.freeArgsTitle} should be provided`)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export function getDefaultCliDescription(): CliDescription {
|
||||
return {
|
||||
version: VERSION,
|
||||
bin: BIN,
|
||||
description: DESCRIPTION,
|
||||
usage: "[-t --tests] [-e --exclude] <module_name1>, <module_name2>, ..",
|
||||
args: {
|
||||
include: {
|
||||
keys: ['--tests', '--include'],
|
||||
help: "Tests to include. Example: MySuite.test1,MySuite.MySubSuite.*,*unix*,!*windows*",
|
||||
default: "*"
|
||||
|
||||
},
|
||||
exclude: {
|
||||
keys: ['--exclude'],
|
||||
help: "Tests to exclude. Example: MySuite.test1,MySuite.MySubSuite.*,*unix*"
|
||||
},
|
||||
ignoredTestSuites: {
|
||||
keys: ['--ignoredTestSuites'],
|
||||
help: "How to deal with ignored test suites",
|
||||
single: true,
|
||||
values: [
|
||||
IgnoredTestSuitesReporting.skip,
|
||||
IgnoredTestSuitesReporting.reportAsIgnoredTest,
|
||||
IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored
|
||||
],
|
||||
valuesHelp: [
|
||||
"don't report ignored test suites",
|
||||
"useful to speedup large ignored test suites",
|
||||
"will cause visiting all inner tests",
|
||||
],
|
||||
default: IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored
|
||||
}
|
||||
},
|
||||
freeArgsTitle: "module_name"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import {KotlinTestRunner} from "./KotlinTestRunner";
|
||||
import {
|
||||
allTest,
|
||||
CompositeTestFilter,
|
||||
KotlinTestsFilter,
|
||||
newKotlinTestsFilter,
|
||||
runWithFilter
|
||||
} from "./KotlinTestsFilter";
|
||||
import {flatMap, println, pushIfNotNull} from "./utils";
|
||||
|
||||
export function configureFiltering(
|
||||
runner: KotlinTestRunner,
|
||||
includeWildcards: string[],
|
||||
excludeWildcards: string[]
|
||||
): KotlinTestRunner {
|
||||
const include: KotlinTestsFilter[] = [];
|
||||
const exclude: KotlinTestsFilter[] = [];
|
||||
|
||||
function collectWildcards(
|
||||
value: string[],
|
||||
positive: KotlinTestsFilter[],
|
||||
negative: KotlinTestsFilter[]
|
||||
) {
|
||||
flatMap(value, (t: string) => t.split(','))
|
||||
.map(t => {
|
||||
if (t.length && t[0] == '!') {
|
||||
pushIfNotNull(negative, newKotlinTestsFilter(t.substring(1)))
|
||||
} else {
|
||||
pushIfNotNull(positive, newKotlinTestsFilter(t))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
collectWildcards(includeWildcards, include, exclude);
|
||||
collectWildcards(excludeWildcards, exclude, include);
|
||||
|
||||
if (include.length == 0 && exclude.length == 0) {
|
||||
return runner
|
||||
} else {
|
||||
if (include.length == 0) {
|
||||
include.push(allTest)
|
||||
}
|
||||
|
||||
const filter = new CompositeTestFilter(include, exclude);
|
||||
|
||||
if (DEBUG) {
|
||||
println(filter.toString());
|
||||
}
|
||||
|
||||
return runWithFilter(runner, filter)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface KotlinTestRunner {
|
||||
suite(name: string, isIgnored: boolean, fn: () => void): void
|
||||
|
||||
test(name: string, isIgnored: boolean, fn: () => void): void
|
||||
}
|
||||
|
||||
export const directRunner: KotlinTestRunner = {
|
||||
suite(name: string, isIgnored: boolean, fn: () => void): void {
|
||||
if (!isIgnored) fn()
|
||||
},
|
||||
test(name: string, isIgnored: boolean, fn: () => void): void {
|
||||
if (!isIgnored) fn()
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
import {KotlinTestRunner} from "./KotlinTestRunner";
|
||||
import {TeamCityMessageData, TeamCityMessagesFlow} from "./TeamCityMessagesFlow";
|
||||
import {Timer} from "./Timer";
|
||||
|
||||
const kotlin_test = require('kotlin-test');
|
||||
|
||||
// don't use enum as it is not minified by uglify
|
||||
export type IgnoredTestSuitesReporting
|
||||
= "skip" | "reportAsIgnoredTest" | "reportAllInnerTestsAsIgnored"
|
||||
export const IgnoredTestSuitesReporting: { [key: string]: IgnoredTestSuitesReporting } = {
|
||||
skip: "skip",
|
||||
reportAsIgnoredTest: "reportAsIgnoredTest",
|
||||
reportAllInnerTestsAsIgnored: "reportAllInnerTestsAsIgnored"
|
||||
};
|
||||
|
||||
// to reduce minified code size
|
||||
function withName(name: string, data?: TeamCityMessageData): TeamCityMessageData {
|
||||
data = data || {};
|
||||
data["name"] = name;
|
||||
return data
|
||||
}
|
||||
|
||||
export function runWithTeamCityReporter(
|
||||
runner: KotlinTestRunner,
|
||||
ignoredTestSuites: IgnoredTestSuitesReporting,
|
||||
teamCity: TeamCityMessagesFlow,
|
||||
timer: Timer<any> | null
|
||||
): KotlinTestRunner {
|
||||
let inIgnoredSuite = false;
|
||||
let currentAssertionResult: { expected: any, actual: any } | null = null;
|
||||
|
||||
kotlin_test.kotlin.test.setAssertHook_4duqou$(function (assertionResult: { expected: any, actual: any }) {
|
||||
currentAssertionResult = assertionResult;
|
||||
});
|
||||
|
||||
return {
|
||||
suite: function (name: string, isIgnored: boolean, fn: () => void) {
|
||||
if (isIgnored) {
|
||||
if (ignoredTestSuites == IgnoredTestSuitesReporting.skip) return;
|
||||
else if (ignoredTestSuites == IgnoredTestSuitesReporting.reportAsIgnoredTest) {
|
||||
teamCity.sendMessage('testIgnored', withName(name, {"suite": true}));
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
teamCity.sendMessage('testSuiteStarted', withName(name));
|
||||
|
||||
// noinspection UnnecessaryLocalVariableJS
|
||||
const alreadyInIgnoredSuite = inIgnoredSuite;
|
||||
if (!alreadyInIgnoredSuite && isIgnored) {
|
||||
inIgnoredSuite = true;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isIgnored && ignoredTestSuites == IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored) {
|
||||
fn();
|
||||
} else {
|
||||
runner.suite(name, isIgnored, fn)
|
||||
}
|
||||
} finally {
|
||||
if (isIgnored && !alreadyInIgnoredSuite) {
|
||||
inIgnoredSuite = false;
|
||||
}
|
||||
|
||||
const data: TeamCityMessageData = withName(name);
|
||||
|
||||
// extension only for Gradle
|
||||
if (isIgnored) data["ignored"] = true;
|
||||
|
||||
teamCity.sendMessage('testSuiteFinished', data);
|
||||
}
|
||||
},
|
||||
test: function (name: string, isIgnored: boolean, fn: () => void) {
|
||||
if (inIgnoredSuite || isIgnored) {
|
||||
teamCity.sendMessage('testIgnored', withName(name));
|
||||
} else {
|
||||
const startTime = timer ? timer.start() : null;
|
||||
teamCity.sendMessage('testStarted', withName(name));
|
||||
try {
|
||||
runner.test(name, isIgnored, fn);
|
||||
} catch (e) {
|
||||
const data: TeamCityMessageData = withName(name, {
|
||||
"message": e.message,
|
||||
"details": e.stack
|
||||
});
|
||||
|
||||
if (currentAssertionResult) {
|
||||
data["type"] = 'comparisonFailure';
|
||||
data["expected"] = currentAssertionResult.expected;
|
||||
data["actual"] = currentAssertionResult.actual;
|
||||
}
|
||||
teamCity.sendMessage('testFailed', data);
|
||||
} finally {
|
||||
currentAssertionResult = null;
|
||||
const data: TeamCityMessageData = withName(name);
|
||||
if (startTime) {
|
||||
data["duration"] = timer!!.end(startTime); // ns to ms
|
||||
}
|
||||
teamCity.sendMessage('testFinished', data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import {escapeRegExp, startsWith, trim} from "./utils";
|
||||
import {KotlinTestRunner} from "./KotlinTestRunner";
|
||||
|
||||
export interface KotlinTestsFilter {
|
||||
mayContainTestsFromSuite(fqn: string): boolean;
|
||||
|
||||
containsTest(fqn: string): boolean;
|
||||
}
|
||||
|
||||
export function runWithFilter(
|
||||
runner: KotlinTestRunner,
|
||||
filter: KotlinTestsFilter,
|
||||
): KotlinTestRunner {
|
||||
let path: string[] = [];
|
||||
|
||||
function pathString() {
|
||||
// skip root
|
||||
if (!path[0]) {
|
||||
return path.slice(1).join('.')
|
||||
} else {
|
||||
return path.join('.')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
suite: function (name: string, isIgnored: boolean, fn: () => void) {
|
||||
path.push(name);
|
||||
|
||||
try {
|
||||
if (path.length > 1 && !filter.mayContainTestsFromSuite(pathString())) return;
|
||||
|
||||
runner.suite(name, isIgnored, fn);
|
||||
} finally {
|
||||
path.pop()
|
||||
}
|
||||
},
|
||||
|
||||
test: function (name: string, isIgnored: boolean, fn: () => void) {
|
||||
path.push(name);
|
||||
|
||||
try {
|
||||
if (!filter.containsTest(pathString())) return;
|
||||
|
||||
runner.test(name, isIgnored, fn);
|
||||
} finally {
|
||||
path.pop()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function newKotlinTestsFilter(wildcard: string | null): KotlinTestsFilter | null {
|
||||
if (wildcard == null) return null;
|
||||
wildcard = trim(wildcard);
|
||||
wildcard = wildcard.replace(/\*+/, '*'); // ** => *
|
||||
if (wildcard.length == 0) return null;
|
||||
else if (wildcard == '*') return allTest;
|
||||
else if (wildcard.indexOf('*') == -1) return new ExactFilter(wildcard);
|
||||
else if (startsWith(wildcard, '*')) return new RegExpKotlinTestsFilter(wildcard);
|
||||
else {
|
||||
// optimize for cases like "Something*", "Something*a*b" and so on.
|
||||
// by adding explicit prefix matcher to not visit unneeded suites
|
||||
// (RegExpKotlinTestsFilter doesn't support suites matching)
|
||||
const [prefix, rest] = wildcard.split('*', 2);
|
||||
return new StartsWithFilter(prefix, rest ? new RegExpKotlinTestsFilter(wildcard) : null)
|
||||
}
|
||||
}
|
||||
|
||||
export const allTest = new class implements KotlinTestsFilter {
|
||||
mayContainTestsFromSuite(fqn: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
containsTest(fqn: string): boolean {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export class StartsWithFilter implements KotlinTestsFilter {
|
||||
constructor(
|
||||
public readonly prefix: string,
|
||||
public readonly filter: RegExpKotlinTestsFilter | null
|
||||
) {
|
||||
}
|
||||
|
||||
isPrefixMatched(fqn: string): boolean {
|
||||
return startsWith(fqn + ".", this.prefix);
|
||||
}
|
||||
|
||||
mayContainTestsFromSuite(fqn: string): boolean {
|
||||
return this.isPrefixMatched(fqn);
|
||||
}
|
||||
|
||||
containsAllTestsFromSuite(fqn: string): boolean {
|
||||
return this.filter == null && this.isPrefixMatched(fqn);
|
||||
}
|
||||
|
||||
containsTest(fqn: string): boolean {
|
||||
return startsWith(fqn, this.prefix)
|
||||
&& (this.filter == null || this.filter.containsTest(fqn));
|
||||
}
|
||||
}
|
||||
|
||||
export class ExactFilter implements KotlinTestsFilter {
|
||||
constructor(public fqn: string) {
|
||||
}
|
||||
|
||||
mayContainTestsFromSuite(fqn: string): boolean {
|
||||
return startsWith(this.fqn, fqn);
|
||||
}
|
||||
|
||||
containsTest(fqn: string): boolean {
|
||||
return fqn === this.fqn;
|
||||
}
|
||||
}
|
||||
|
||||
export class RegExpKotlinTestsFilter implements KotlinTestsFilter {
|
||||
public readonly regexp: RegExp;
|
||||
|
||||
constructor(wildcard: string) {
|
||||
this.regexp = RegExp("^" + wildcard
|
||||
.split('*')
|
||||
.map(it => escapeRegExp(it))
|
||||
.join('.*') + "$"
|
||||
);
|
||||
}
|
||||
|
||||
mayContainTestsFromSuite(fqn: string): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
containsTest(fqn: string): boolean {
|
||||
return this.regexp!.test(fqn)
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.regexp.toString()
|
||||
}
|
||||
}
|
||||
|
||||
export class CompositeTestFilter implements KotlinTestsFilter {
|
||||
private readonly excludePrefix: StartsWithFilter[] = [];
|
||||
|
||||
constructor(
|
||||
public include: KotlinTestsFilter[],
|
||||
public exclude: KotlinTestsFilter[]
|
||||
) {
|
||||
this.exclude.forEach(it => {
|
||||
if (it instanceof StartsWithFilter && it.filter == null)
|
||||
this.excludePrefix.push(it)
|
||||
})
|
||||
}
|
||||
|
||||
mayContainTestsFromSuite(fqn: string): boolean {
|
||||
for (const excl of this.excludePrefix) {
|
||||
if (excl.containsAllTestsFromSuite(fqn)) return false
|
||||
}
|
||||
for (const incl of this.include) {
|
||||
if (incl.mayContainTestsFromSuite(fqn)) return true
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
containsTest(fqn: string): boolean {
|
||||
for (const excl of this.exclude) {
|
||||
if (excl.containsTest(fqn)) return false
|
||||
}
|
||||
for (const incl of this.include) {
|
||||
if (incl.containsTest(fqn)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
allTest,
|
||||
CompositeTestFilter,
|
||||
ExactFilter,
|
||||
RegExpKotlinTestsFilter,
|
||||
StartsWithFilter
|
||||
} from "./KotlinTestsFilter";
|
||||
|
||||
declare const DEBUG: boolean;
|
||||
|
||||
if (DEBUG) {
|
||||
allTest.toString = function () {
|
||||
return "(ALL)"
|
||||
};
|
||||
|
||||
StartsWithFilter.prototype.toString = function (): string {
|
||||
return "(STARTS WITH " + this.prefix + (this.filter ? (" AND " + this.filter) : "") + ")"
|
||||
};
|
||||
|
||||
RegExpKotlinTestsFilter.prototype.toString = function (): string {
|
||||
return "(REGEXP " + this.regexp + ")"
|
||||
};
|
||||
|
||||
ExactFilter.prototype.toString = function (): string {
|
||||
return "(EXACT " + this.fqn + ")"
|
||||
};
|
||||
|
||||
CompositeTestFilter.prototype.toString = function (): string {
|
||||
return this.include.map(it => it.toString()).join(" OR ")
|
||||
+ "\n NOT (" + this.exclude.map(it => it.toString()).join(" OR ") + ")"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import {dateTimeWithoutTimeZone, newFlowId, tcEscape} from "./utils"
|
||||
|
||||
export type TeamCityMessageData = { [key: string]: any }
|
||||
|
||||
export class TeamCityMessagesFlow {
|
||||
public readonly id: number;
|
||||
|
||||
constructor(id: number | null, private readonly send: (payload: string) => void) {
|
||||
this.id = id || newFlowId()
|
||||
}
|
||||
|
||||
sendMessage(type: string, args: TeamCityMessageData) {
|
||||
args['flowId'] = this.id;
|
||||
args['timestamp'] = dateTimeWithoutTimeZone();
|
||||
|
||||
const serializedArgs = Object
|
||||
.keys(args)
|
||||
.map((key) => `${key}='${tcEscape(args[key])}'`)
|
||||
.join(' ');
|
||||
|
||||
this.send(`##teamcity[${type} ${serializedArgs}]`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface Timer<T> {
|
||||
start(): T
|
||||
|
||||
end(start: T): number
|
||||
}
|
||||
|
||||
export const hrTimer: Timer<[number, number]> = {
|
||||
start(): [number, number] {
|
||||
return process.hrtime();
|
||||
},
|
||||
end(start: [number, number]): number {
|
||||
const elapsedHr = process.hrtime(start);
|
||||
return elapsedHr[0] + (elapsedHr[1] / 1e6); // ns to ms
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
|
||||
/**
|
||||
* from teamcity-service-messages
|
||||
* Copyright (c) 2013 Aaron Forsander
|
||||
*
|
||||
* Escape string for TeamCity output.
|
||||
* @see https://confluence.jetbrains.com/display/TCD65/Build+Script+Interaction+with+TeamCity#BuildScriptInteractionwithTeamCity-servMsgsServiceMessages
|
||||
*/
|
||||
export function tcEscape(str: string): string {
|
||||
if (!str) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return str
|
||||
.toString()
|
||||
.replace(/\|/g, '||')
|
||||
.replace(/\n/g, '|n')
|
||||
.replace(/\r/g, '|r')
|
||||
.replace(/\[/g, '|[')
|
||||
.replace(/\]/g, '|]')
|
||||
.replace(/\u0085/g, '|x') // next line
|
||||
.replace(/\u2028/g, '|l') // line separator
|
||||
.replace(/\u2029/g, '|p') // paragraph separator
|
||||
.replace(/'/g, '|\'');
|
||||
}
|
||||
|
||||
/**
|
||||
* From teamcity-service-messages.
|
||||
* Copyright 2013 Aaron Forsander
|
||||
*/
|
||||
export function newFlowId(): number {
|
||||
return Math.floor(Math.random() * (1e10 - 1e6 + 1)) + 1e6;
|
||||
}
|
||||
|
||||
/**
|
||||
* From teamcity-service-messages.
|
||||
* Copyright 2013 Aaron Forsander
|
||||
*/
|
||||
export function dateTimeWithoutTimeZone(): string {
|
||||
// TeamCity not fully support ISO 8601 (see TW-36173) so we need to cut off 'Z' at the end.
|
||||
return new Date().toISOString().slice(0, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* From lodash.
|
||||
* Copyright JS Foundation and other contributors <https://js.foundation/>
|
||||
*/
|
||||
export function startsWith(string: string, target: string) {
|
||||
return string.slice(0, target.length) == target;
|
||||
}
|
||||
|
||||
/**
|
||||
* From lodash.
|
||||
* Copyright JS Foundation and other contributors <https://js.foundation/>
|
||||
*/
|
||||
export function trim(str: string): string {
|
||||
return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to match `RegExp`
|
||||
* [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
|
||||
*/
|
||||
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g,
|
||||
reHasRegExpChar = RegExp(reRegExpChar.source);
|
||||
|
||||
/**
|
||||
* Escapes the `RegExp` special characters "^", "$", "\", ".", "*", "+",
|
||||
* "?", "(", ")", "[", "]", "{", "}", and "|" in `string`.
|
||||
*
|
||||
* From lodash.
|
||||
* Copyright JS Foundation and other contributors <https://js.foundation/>
|
||||
*/
|
||||
export function escapeRegExp(string: string) {
|
||||
return (string && reHasRegExpChar.test(string))
|
||||
? string.replace(reRegExpChar, '\\$&')
|
||||
: string;
|
||||
}
|
||||
|
||||
export function pushIfNotNull<T>(list: T[], value: T) {
|
||||
if (value !== null) list.push(value)
|
||||
}
|
||||
|
||||
export function flatMap<T>(arr: T[], f: (item: T) => T[]): T[] {
|
||||
const result: T[] = [];
|
||||
arr.forEach(item => {
|
||||
f(item).forEach(x => {
|
||||
result.push(x)
|
||||
})
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function println(message ?: string) {
|
||||
console.log(message)
|
||||
}
|
||||
Reference in New Issue
Block a user