diff --git a/README.md b/README.md index 6e4091b..34f0d1c 100644 --- a/README.md +++ b/README.md @@ -64,24 +64,24 @@ new HashMap( ### Methods -| Method | Description | Time Complexity | -|--------|-------------|-----------------| -| `set(key: K, value: V): void` | Insert or update a key-value pair | O(1) average | -| `get(key: K): V \| undefined` | Retrieve value by key | O(1) average | -| `has(key: K): boolean` | Check if key exists | O(1) average | -| `delete(key: K): boolean` | Remove entry by key | O(1) average | -| `clear(): void` | Remove all entries | O(n) | -| `keys(): IterableIterator` | Iterator over keys | O(n) | -| `values(): IterableIterator` | Iterator over values | O(n) | -| `entries(): IterableIterator<[K, V]>` | Iterator over entries | O(n) | -| `forEach(callback): void` | Execute callback for each entry | O(n) | +| Method | Description | Time Complexity | +| ------------------------------------- | --------------------------------- | --------------- | +| `set(key: K, value: V): void` | Insert or update a key-value pair | O(1) average | +| `get(key: K): V \| undefined` | Retrieve value by key | O(1) average | +| `has(key: K): boolean` | Check if key exists | O(1) average | +| `delete(key: K): boolean` | Remove entry by key | O(1) average | +| `clear(): void` | Remove all entries | O(n) | +| `keys(): IterableIterator` | Iterator over keys | O(n) | +| `values(): IterableIterator` | Iterator over values | O(n) | +| `entries(): IterableIterator<[K, V]>` | Iterator over entries | O(n) | +| `forEach(callback): void` | Execute callback for each entry | O(n) | ### Properties -| Property | Description | -|----------|-------------| -| `size` | Number of key-value pairs | -| `capacity` | Current number of buckets | +| Property | Description | +| ------------ | ------------------------------------- | +| `size` | Number of key-value pairs | +| `capacity` | Current number of buckets | | `loadFactor` | Current load factor (size / capacity) | ## Advanced Usage @@ -108,7 +108,7 @@ class CaseInsensitiveHashFunction implements IHashFunction { const map = new HashMap( 16, 0.75, - new CaseInsensitiveHashFunction() + new CaseInsensitiveHashFunction(), ); map.set("Hello", 1); @@ -122,11 +122,7 @@ Use the built-in `NumericHashFunction` for better distribution with numeric keys ```typescript import { HashMap, NumericHashFunction } from "@techniker-me/hash-map"; -const map = new HashMap( - 16, - 0.75, - new NumericHashFunction() -); +const map = new HashMap(16, 0.75, new NumericHashFunction()); map.set(12345, "value1"); map.set(67890, "value2"); @@ -155,25 +151,30 @@ console.log(user?.name); // "Alice" This implementation adheres to all five SOLID principles: ### 1. Single Responsibility Principle (SRP) + - `HashMap` - Manages hash map operations - `HashNode` - Stores key-value pairs - `DefaultHashFunction` - Handles hashing logic - Each class has one clear purpose ### 2. Open/Closed Principle (OCP) + - Extensible through custom hash functions - Core implementation is closed for modification ### 3. Liskov Substitution Principle (LSP) + - All implementations correctly implement their interfaces - Subtypes can replace their base types without breaking functionality ### 4. Interface Segregation Principle (ISP) + - `IHashMap` - Focused map operations - `IHashFunction` - Minimal hashing interface - Clients depend only on interfaces they use ### 5. Dependency Inversion Principle (DIP) + - Depends on `IHashFunction` abstraction, not concrete implementations - High-level modules don't depend on low-level modules @@ -230,6 +231,7 @@ bun test --coverage ``` **Test Coverage: 100%** ✅ + - 66 comprehensive tests - 1,168 assertions - All edge cases covered diff --git a/eslint.config.ts b/eslint.config.ts index c5ced9a..4758a2e 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -8,11 +8,31 @@ import { defineConfig } from "eslint/config"; export default defineConfig([ { - ignores: ["dist/**", "node_modules/**", "*.min.js"] + ignores: ["dist/**", "node_modules/**", "*.min.js"], + }, + { + files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], + plugins: { js }, + extends: ["js/recommended"], + languageOptions: { globals: globals.node }, }, - { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } }, tseslint.configs.recommended, - { files: ["**/*.json"], plugins: { json }, language: "json/json", extends: ["json/recommended"] }, - { files: ["**/*.md"], plugins: { markdown }, language: "markdown/commonmark", extends: ["markdown/recommended"] }, - { files: ["**/*.css"], plugins: { css }, language: "css/css", extends: ["css/recommended"] }, + { + files: ["**/*.json"], + plugins: { json }, + language: "json/json", + extends: ["json/recommended"], + }, + { + files: ["**/*.md"], + plugins: { markdown }, + language: "markdown/commonmark", + extends: ["markdown/recommended"], + }, + { + files: ["**/*.css"], + plugins: { css }, + language: "css/css", + extends: ["css/recommended"], + }, ]); diff --git a/package.json b/package.json index 933e9ad..c66e65c 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,25 @@ { "name": "@techniker-me/hash-map", - "version": "1.0.8", + "version": "1.0.9", "description": "A robust HashMap implementation following OOP SOLID principles", "type": "module", - "main": "browser/index.ts", - "module": "node/index.js", - "types": "types/index.d.ts", + "main": "./node/index.js", + "module": "./browser/index.js", + "types": "./types/index.d.ts", "exports": { ".": { - "node": "node/index.js", - "browser": "browser/index.js" + "types": "./types/index.d.ts", + "node": { + "import": "./node/index.js", + "default": "./node/index.js" + }, + "browser": { + "import": "./browser/index.js", + "default": "./browser/index.js" + }, + "default": "./node/index.js" }, - "./types": "types/index.d.ts" + "./types": "./types/index.d.ts" }, "files": [ "node", diff --git a/scripts/prepare-package-json.sh b/scripts/prepare-package-json.sh index a516f9a..402f418 100755 --- a/scripts/prepare-package-json.sh +++ b/scripts/prepare-package-json.sh @@ -13,4 +13,4 @@ if [ ! -d "${distDirectory}" ]; then fi echo "Preparing [package.json] to [${distDirectory}]" -jq '{name, version, author, type, types, exports, files, publishConfig}' "${packageJsonPath}" > "${distDirectory}/package.json" +jq '{name, version, author, type, main, module, types, exports, files, publishConfig}' "${packageJsonPath}" > "${distDirectory}/package.json" diff --git a/scripts/test-tap-teamcity.ts b/scripts/test-tap-teamcity.ts index 23b132a..b496a64 100755 --- a/scripts/test-tap-teamcity.ts +++ b/scripts/test-tap-teamcity.ts @@ -16,10 +16,13 @@ async function convertTAPToTeamCity() { return; } - const proc = Bun.spawn(["bun", "test", "--reporter=tap", ...process.argv.slice(2)], { - stdout: "pipe", - stderr: "pipe", - }); + const proc = Bun.spawn( + ["bun", "test", "--reporter=tap", ...process.argv.slice(2)], + { + stdout: "pipe", + stderr: "pipe", + }, + ); const decoder = new TextDecoder(); let currentSuite = "Tests"; @@ -42,11 +45,15 @@ async function convertTAPToTeamCity() { // ok 1 - test description // not ok 2 - failed test // # describe block - + if (line.startsWith("# ")) { // Suite/describe block const suiteName = line.substring(2).trim(); - if (suiteName && !suiteName.startsWith("tests") && !suiteName.startsWith("pass")) { + if ( + suiteName && + !suiteName.startsWith("tests") && + !suiteName.startsWith("pass") + ) { if (testCount > 0) { TeamcityReporter.reportSuiteEnd(currentSuite); } @@ -67,12 +74,11 @@ async function convertTAPToTeamCity() { if (not) { // Test failed - TeamcityReporter.reportTestFailed( - fullName, - "Test failed", - line, - { comparisonFailure: false, expected: "", actual: "" } - ); + TeamcityReporter.reportTestFailed(fullName, "Test failed", line, { + comparisonFailure: false, + expected: "", + actual: "", + }); } TeamcityReporter.reportTestEnd(fullName); @@ -96,4 +102,3 @@ convertTAPToTeamCity().catch((error) => { console.error("Error:", error); process.exit(1); }); - diff --git a/scripts/test-teamcity.ts b/scripts/test-teamcity.ts index 29981ba..23243af 100755 --- a/scripts/test-teamcity.ts +++ b/scripts/test-teamcity.ts @@ -6,6 +6,7 @@ const isTeamCity = process.env.TEAMCITY_VERSION !== undefined; // Strip ANSI color codes function stripAnsi(str: string): string { + // eslint-disable-next-line no-control-regex return str.replace(/\x1b\[[0-9;]*m/g, ""); } @@ -32,7 +33,7 @@ async function runTests() { }); const suiteStack: string[] = []; - + // Read stdout as text const output = await new Response(proc.stdout).text(); const lines = output.split("\n"); @@ -46,8 +47,11 @@ async function runTests() { // Check for test suite/describe blocks (files ending with .test.ts) if (line.match(/tests\/.+\.test\.ts:$/)) { - const fileName = line.replace(":", "").replace("tests/", "").replace(".test.ts", ""); - + const fileName = line + .replace(":", "") + .replace("tests/", "") + .replace(".test.ts", ""); + // End previous suite if exists while (suiteStack.length > 0) { const oldSuite = suiteStack.pop(); @@ -73,12 +77,11 @@ async function runTests() { if (failMatch) { const testName = failMatch[1].trim(); TeamcityReporter.reportTestStart(testName); - TeamcityReporter.reportTestFailed( - testName, - "Test failed", - line, - { comparisonFailure: false, expected: "", actual: "" } - ); + TeamcityReporter.reportTestFailed(testName, "Test failed", line, { + comparisonFailure: false, + expected: "", + actual: "", + }); TeamcityReporter.reportTestEnd(testName); } } diff --git a/src/core/HashMap.ts b/src/core/HashMap.ts index 728f04a..faedd7b 100644 --- a/src/core/HashMap.ts +++ b/src/core/HashMap.ts @@ -1,7 +1,7 @@ -import type { IHashMap } from "../interfaces/IHashMap.ts"; -import type { IHashFunction } from "../interfaces/IHashFunction.ts"; -import { HashNode } from "../models/HashNode.ts"; -import { DefaultHashFunction } from "../hash-functions/DefaultHashFunction.ts"; +import type { IHashMap } from "../interfaces/IHashMap"; +import type { IHashFunction } from "../interfaces/IHashFunction"; +import { HashNode } from "../models/HashNode"; +import { DefaultHashFunction } from "../hash-functions/DefaultHashFunction"; /** * HashMap implementation using separate chaining for collision resolution. diff --git a/src/hash-functions/DefaultHashFunction.ts b/src/hash-functions/DefaultHashFunction.ts index 906fc36..5d898a2 100644 --- a/src/hash-functions/DefaultHashFunction.ts +++ b/src/hash-functions/DefaultHashFunction.ts @@ -1,4 +1,4 @@ -import type { IHashFunction } from "../interfaces/IHashFunction.ts"; +import type { IHashFunction } from "../interfaces/IHashFunction"; /** * Default hash function implementation. diff --git a/src/hash-functions/NumericHashFunction.ts b/src/hash-functions/NumericHashFunction.ts index a32fdef..db5fd4b 100644 --- a/src/hash-functions/NumericHashFunction.ts +++ b/src/hash-functions/NumericHashFunction.ts @@ -1,4 +1,4 @@ -import type { IHashFunction } from "../interfaces/IHashFunction.ts"; +import type { IHashFunction } from "../interfaces/IHashFunction"; /** * Specialized hash function for numeric keys. diff --git a/src/index.ts b/src/index.ts index 8d9db32..e6fbe8f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,15 +19,15 @@ */ // Core implementation -export { HashMap } from "./core/HashMap.ts"; +export { HashMap } from "./core/HashMap"; // Interfaces -export type { IHashMap } from "./interfaces/IHashMap.ts"; -export type { IHashFunction } from "./interfaces/IHashFunction.ts"; +export type { IHashMap } from "./interfaces/IHashMap"; +export type { IHashFunction } from "./interfaces/IHashFunction"; // Models -export { HashNode } from "./models/HashNode.ts"; +export { HashNode } from "./models/HashNode"; // Hash functions -export { DefaultHashFunction } from "./hash-functions/DefaultHashFunction.ts"; -export { NumericHashFunction } from "./hash-functions/NumericHashFunction.ts"; +export { DefaultHashFunction } from "./hash-functions/DefaultHashFunction"; +export { NumericHashFunction } from "./hash-functions/NumericHashFunction"; diff --git a/tests/HashFunctions.test.ts b/tests/HashFunctions.test.ts index 31dade8..a96d8f5 100644 --- a/tests/HashFunctions.test.ts +++ b/tests/HashFunctions.test.ts @@ -10,7 +10,7 @@ describe("DefaultHashFunction", () => { it("should hash string keys", () => { const hash1 = hashFn.hash("hello", capacity); const hash2 = hashFn.hash("world", capacity); - + expect(hash1).toBeGreaterThanOrEqual(0); expect(hash1).toBeLessThan(capacity); expect(hash2).toBeGreaterThanOrEqual(0); @@ -22,7 +22,7 @@ describe("DefaultHashFunction", () => { const hash2 = hashFn.hash(3.14, capacity); const hash3 = hashFn.hash(0, capacity); const hash4 = hashFn.hash(-10, capacity); - + expect(hash1).toBeGreaterThanOrEqual(0); expect(hash1).toBeLessThan(capacity); expect(hash2).toBeGreaterThanOrEqual(0); @@ -33,7 +33,7 @@ describe("DefaultHashFunction", () => { it("should hash boolean keys", () => { const hashTrue = hashFn.hash(true, capacity); const hashFalse = hashFn.hash(false, capacity); - + expect(hashTrue).toBeGreaterThanOrEqual(0); expect(hashTrue).toBeLessThan(capacity); expect(hashFalse).toBeGreaterThanOrEqual(0); @@ -57,7 +57,7 @@ describe("DefaultHashFunction", () => { it("should hash simple objects", () => { const obj = { name: "Alice", age: 30 }; const hash = hashFn.hash(obj, capacity); - + expect(hash).toBeGreaterThanOrEqual(0); expect(hash).toBeLessThan(capacity); }); @@ -65,7 +65,7 @@ describe("DefaultHashFunction", () => { it("should hash arrays", () => { const arr = [1, 2, 3, 4, 5]; const hash = hashFn.hash(arr, capacity); - + expect(hash).toBeGreaterThanOrEqual(0); expect(hash).toBeLessThan(capacity); }); @@ -76,12 +76,12 @@ describe("DefaultHashFunction", () => { name: "Bob", address: { city: "NYC", - zip: "10001" - } - } + zip: "10001", + }, + }, }; const hash = hashFn.hash(nested, capacity); - + expect(hash).toBeGreaterThanOrEqual(0); expect(hash).toBeLessThan(capacity); }); @@ -89,7 +89,7 @@ describe("DefaultHashFunction", () => { it("should handle circular references gracefully", () => { const circular: { name: string; self?: unknown } = { name: "test" }; circular.self = circular; // Create circular reference - + // Should not throw, should fall back to Object.prototype.toString const hash = hashFn.hash(circular, capacity); expect(hash).toBeGreaterThanOrEqual(0); @@ -99,7 +99,7 @@ describe("DefaultHashFunction", () => { it("should hash Date objects", () => { const date = new Date("2024-01-01"); const hash = hashFn.hash(date, capacity); - + expect(hash).toBeGreaterThanOrEqual(0); expect(hash).toBeLessThan(capacity); }); @@ -107,7 +107,7 @@ describe("DefaultHashFunction", () => { it("should hash RegExp objects", () => { const regex = /test/g; const hash = hashFn.hash(regex, capacity); - + expect(hash).toBeGreaterThanOrEqual(0); expect(hash).toBeLessThan(capacity); }); @@ -115,7 +115,7 @@ describe("DefaultHashFunction", () => { it("should hash Error objects", () => { const error = new Error("Test error"); const hash = hashFn.hash(error, capacity); - + expect(hash).toBeGreaterThanOrEqual(0); expect(hash).toBeLessThan(capacity); }); @@ -143,7 +143,7 @@ describe("DefaultHashFunction", () => { it("should hash symbols", () => { const sym = Symbol("test"); const hash = hashFn.hash(sym, capacity); - + expect(hash).toBeGreaterThanOrEqual(0); expect(hash).toBeLessThan(capacity); }); @@ -151,7 +151,7 @@ describe("DefaultHashFunction", () => { it("should hash bigint", () => { const bigInt = BigInt(12345678901234567890n); const hash = hashFn.hash(bigInt, capacity); - + expect(hash).toBeGreaterThanOrEqual(0); expect(hash).toBeLessThan(capacity); }); @@ -162,14 +162,14 @@ describe("DefaultHashFunction", () => { const key = "test-key"; const hash1 = hashFn.hash(key, capacity); const hash2 = hashFn.hash(key, capacity); - + expect(hash1).toBe(hash2); }); it("should return different hashes for different keys (usually)", () => { const hash1 = hashFn.hash("key1", capacity); const hash2 = hashFn.hash("key2", capacity); - + // Note: They COULD collide, but unlikely expect(hash1).toBeGreaterThanOrEqual(0); expect(hash2).toBeGreaterThanOrEqual(0); @@ -180,7 +180,7 @@ describe("DefaultHashFunction", () => { const hash8 = hashFn.hash(key, 8); const hash16 = hashFn.hash(key, 16); const hash32 = hashFn.hash(key, 32); - + expect(hash8).toBeLessThan(8); expect(hash16).toBeLessThan(16); expect(hash32).toBeLessThan(32); @@ -196,7 +196,7 @@ describe("NumericHashFunction", () => { it("should hash positive integers", () => { const hash1 = hashFn.hash(42, capacity); const hash2 = hashFn.hash(100, capacity); - + expect(hash1).toBeGreaterThanOrEqual(0); expect(hash1).toBeLessThan(capacity); expect(hash2).toBeGreaterThanOrEqual(0); @@ -205,14 +205,14 @@ describe("NumericHashFunction", () => { it("should hash negative integers", () => { const hash = hashFn.hash(-42, capacity); - + expect(hash).toBeGreaterThanOrEqual(0); expect(hash).toBeLessThan(capacity); }); it("should hash zero", () => { const hash = hashFn.hash(0, capacity); - + expect(hash).toBeGreaterThanOrEqual(0); expect(hash).toBeLessThan(capacity); }); @@ -220,7 +220,7 @@ describe("NumericHashFunction", () => { it("should hash floating point numbers", () => { const hash1 = hashFn.hash(3.14159, capacity); const hash2 = hashFn.hash(2.71828, capacity); - + expect(hash1).toBeGreaterThanOrEqual(0); expect(hash1).toBeLessThan(capacity); expect(hash2).toBeGreaterThanOrEqual(0); @@ -229,14 +229,14 @@ describe("NumericHashFunction", () => { it("should hash very large numbers", () => { const hash = hashFn.hash(Number.MAX_SAFE_INTEGER, capacity); - + expect(hash).toBeGreaterThanOrEqual(0); expect(hash).toBeLessThan(capacity); }); it("should hash very small numbers", () => { const hash = hashFn.hash(Number.MIN_VALUE, capacity); - + expect(hash).toBeGreaterThanOrEqual(0); expect(hash).toBeLessThan(capacity); }); @@ -245,21 +245,21 @@ describe("NumericHashFunction", () => { describe("special numeric values", () => { it("should handle Infinity", () => { const hash = hashFn.hash(Infinity, capacity); - + // Should return 0 for non-finite numbers expect(hash).toBe(0); }); it("should handle negative Infinity", () => { const hash = hashFn.hash(-Infinity, capacity); - + // Should return 0 for non-finite numbers expect(hash).toBe(0); }); it("should handle NaN", () => { const hash = hashFn.hash(NaN, capacity); - + // Should return 0 for non-finite numbers expect(hash).toBe(0); }); @@ -270,7 +270,7 @@ describe("NumericHashFunction", () => { const num = 42; const hash1 = hashFn.hash(num, capacity); const hash2 = hashFn.hash(num, capacity); - + expect(hash1).toBe(hash2); }); @@ -279,7 +279,7 @@ describe("NumericHashFunction", () => { const hash8 = hashFn.hash(num, 8); const hash16 = hashFn.hash(num, 16); const hash32 = hashFn.hash(num, 32); - + expect(hash8).toBeLessThan(8); expect(hash16).toBeLessThan(16); expect(hash32).toBeLessThan(32); @@ -287,12 +287,12 @@ describe("NumericHashFunction", () => { it("should distribute numbers evenly", () => { const hashes = new Set(); - + // Hash 100 sequential numbers for (let i = 0; i < 100; i++) { hashes.add(hashFn.hash(i, capacity)); } - + // Should have good distribution (not all in one bucket) expect(hashes.size).toBeGreaterThan(1); }); @@ -303,7 +303,7 @@ describe("NumericHashFunction", () => { const hash1 = hashFn.hash(-1, capacity); const hash2 = hashFn.hash(-100, capacity); const hash3 = hashFn.hash(-3.14, capacity); - + expect(hash1).toBeGreaterThanOrEqual(0); expect(hash1).toBeLessThan(capacity); expect(hash2).toBeGreaterThanOrEqual(0); @@ -315,10 +315,9 @@ describe("NumericHashFunction", () => { it("should handle same absolute value differently for positive and negative", () => { const hashPos = hashFn.hash(42, capacity); const hashNeg = hashFn.hash(-42, capacity); - + // They should hash to the same bucket (due to Math.abs in implementation) expect(hashPos).toBe(hashNeg); }); }); }); - diff --git a/tests/HashMap.test.ts b/tests/HashMap.test.ts index ff56c85..7a16ea7 100644 --- a/tests/HashMap.test.ts +++ b/tests/HashMap.test.ts @@ -253,7 +253,7 @@ describe("HashMap", () => { const numMap = new HashMap( 16, 0.75, - new NumericHashFunction() + new NumericHashFunction(), ); numMap.set(123, "value1"); @@ -273,7 +273,7 @@ describe("HashMap", () => { const customMap = new HashMap( 8, 0.75, - new SimpleHashFunction() + new SimpleHashFunction(), ); customMap.set("hi", "short"); @@ -359,4 +359,3 @@ describe("HashMap", () => { }); }); }); - diff --git a/tests/TeamcityReporter.ts b/tests/TeamcityReporter.ts index 0654b4d..5526d99 100644 --- a/tests/TeamcityReporter.ts +++ b/tests/TeamcityReporter.ts @@ -1,60 +1,79 @@ - export class TeamcityReporter { - /** - * Escape special characters for TeamCity service messages - * https://www.jetbrains.com/help/teamcity/service-messages.html#Escaped+values - */ - private static escape(str: string): string { - return str - .replace(/\|/g, "||") - .replace(/'/g, "|'") - .replace(/\n/g, "|n") - .replace(/\r/g, "|r") - .replace(/\[/g, "|[") - .replace(/]/g, "|]"); + /** + * Escape special characters for TeamCity service messages + * https://www.jetbrains.com/help/teamcity/service-messages.html#Escaped+values + */ + private static escape(str: string): string { + return str + .replace(/\|/g, "||") + .replace(/'/g, "|'") + .replace(/\n/g, "|n") + .replace(/\r/g, "|r") + .replace(/\[/g, "|[") + .replace(/]/g, "|]"); + } + + public static reportSuiteStart(suiteName: string): void { + console.log( + `##teamcity[testSuiteStarted name='${this.escape(suiteName)}']`, + ); + } + + public static reportSuiteEnd(suiteName: string): void { + console.log( + `##teamcity[testSuiteFinished name='${this.escape(suiteName)}']`, + ); + } + + public static reportTestStart(testName: string): void { + console.log(`##teamcity[testStarted name='${this.escape(testName)}']`); + } + + public static reportTestFailed( + testName: string, + failureMessage: string, + details: string, + { + comparisonFailure, + expected, + actual, + }: { comparisonFailure: boolean; expected: string; actual: string }, + ): void { + const attrs = [ + `name='${this.escape(testName)}'`, + `message='${this.escape(failureMessage)}'`, + `details='${this.escape(details)}'`, + ]; + + if (comparisonFailure) { + attrs.push(`type='comparisonFailure'`); + attrs.push(`expected='${this.escape(expected)}'`); + attrs.push(`actual='${this.escape(actual)}'`); } - public static reportSuiteStart(suiteName: string): void { - console.log(`##teamcity[testSuiteStarted name='${this.escape(suiteName)}']`); - } + console.log(`##teamcity[testFailed ${attrs.join(" ")}]`); + } - public static reportSuiteEnd(suiteName: string): void { - console.log(`##teamcity[testSuiteFinished name='${this.escape(suiteName)}']`); - } + public static reportTestEnd(testName: string, duration?: number): void { + const durationAttr = + duration !== undefined ? ` duration='${duration}'` : ""; + console.log( + `##teamcity[testFinished name='${this.escape(testName)}'${durationAttr}]`, + ); + } - public static reportTestStart(testName: string): void { - console.log(`##teamcity[testStarted name='${this.escape(testName)}']`); - } + public static reportTestError(testName: string, error: Error): void { + const message = this.escape(error.message || "Unknown error"); + const details = this.escape(error.stack || ""); + console.log( + `##teamcity[testFailed name='${this.escape(testName)}' message='${message}' details='${details}']`, + ); + } - public static reportTestFailed(testName: string, failureMessage: string, details: string, {comparisonFailure, expected, actual}: {comparisonFailure: boolean, expected: string, actual: string}): void { - const attrs = [ - `name='${this.escape(testName)}'`, - `message='${this.escape(failureMessage)}'`, - `details='${this.escape(details)}'` - ]; - - if (comparisonFailure) { - attrs.push(`type='comparisonFailure'`); - attrs.push(`expected='${this.escape(expected)}'`); - attrs.push(`actual='${this.escape(actual)}'`); - } - - console.log(`##teamcity[testFailed ${attrs.join(' ')}]`); - } - - public static reportTestEnd(testName: string, duration?: number): void { - const durationAttr = duration !== undefined ? ` duration='${duration}'` : ''; - console.log(`##teamcity[testFinished name='${this.escape(testName)}'${durationAttr}]`); - } - - public static reportTestError(testName: string, error: Error): void { - const message = this.escape(error.message || 'Unknown error'); - const details = this.escape(error.stack || ''); - console.log(`##teamcity[testFailed name='${this.escape(testName)}' message='${message}' details='${details}']`); - } - - public static reportTestIgnored(testName: string, message?: string): void { - const msgAttr = message ? ` message='${this.escape(message)}'` : ''; - console.log(`##teamcity[testIgnored name='${this.escape(testName)}'${msgAttr}]`); - } -} \ No newline at end of file + public static reportTestIgnored(testName: string, message?: string): void { + const msgAttr = message ? ` message='${this.escape(message)}'` : ""; + console.log( + `##teamcity[testIgnored name='${this.escape(testName)}'${msgAttr}]`, + ); + } +} diff --git a/tests/setup.ts b/tests/setup.ts index 1451bb5..88b224a 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -39,7 +39,7 @@ export function setupTeamCityReporting() { name: string, message: string, details: string, - comparison?: { expected: string; actual: string } + comparison?: { expected: string; actual: string }, ) => { TeamcityReporter.reportTestFailed(name, message, details, { comparisonFailure: !!comparison, @@ -52,4 +52,3 @@ export function setupTeamCityReporting() { }, }; } - diff --git a/tsconfig.d.json b/tsconfig.d.json index 9755af9..e163e87 100644 --- a/tsconfig.d.json +++ b/tsconfig.d.json @@ -6,8 +6,8 @@ "moduleResolution": "bundler", "esModuleInterop": true, - "allowImportingTsExtensions": true, - "allowArbitraryExtensions": true, + "allowImportingTsExtensions": false, + "allowArbitraryExtensions": false, "isolatedModules": true, "declaration": true, "emitDeclarationOnly": true, @@ -22,5 +22,5 @@ "noPropertyAccessFromIndexSignature": false }, "include": ["src"], - "exclude": ["dist", "test"] + "exclude": ["dist", "test", "src/examples"] }