|
11 | 11 | //------------------------------------------------------------------------------ |
12 | 12 |
|
13 | 13 | const assert = require("node:assert"), |
| 14 | + { existsSync, readFileSync } = require("node:fs"), |
14 | 15 | util = require("node:util"), |
15 | 16 | path = require("node:path"), |
16 | 17 | equal = require("fast-deep-equal"), |
@@ -666,6 +667,177 @@ function getInvocationLocation(relative = getInvocationLocation) { |
666 | 667 | return stack.split("\n")[1].replace(/.*?\(/u, "").replace(/\)$/u, ""); |
667 | 668 | } |
668 | 669 |
|
| 670 | +/** |
| 671 | + * Estimates the location of the test case in the source file. |
| 672 | + * @param {Function} invoker The method that runs the tests. |
| 673 | + * @returns {(key: string) => string} The lazy resolver for the estimated location of the test case. |
| 674 | + */ |
| 675 | +function buildLazyTestLocationEstimator(invoker) { |
| 676 | + const invocationLocation = getInvocationLocation(invoker); |
| 677 | + let testLocations = null; |
| 678 | + return key => { |
| 679 | + if (testLocations === null) { |
| 680 | + let [sourceFile, sourceLine = "1", sourceColumn = "1"] = |
| 681 | + invocationLocation |
| 682 | + .replace(/:\\/u, "\\\\") // Windows workaround for C:\\ |
| 683 | + .split(":"); |
| 684 | + sourceFile = sourceFile.replace(/\\\\/u, ":\\"); |
| 685 | + sourceLine = Number(sourceLine); |
| 686 | + sourceColumn = Number(sourceColumn); |
| 687 | + testLocations = { root: invocationLocation }; |
| 688 | + |
| 689 | + if (existsSync(sourceFile)) { |
| 690 | + let content = readFileSync(sourceFile, "utf8") |
| 691 | + .split("\n") |
| 692 | + .slice(sourceLine - 1); |
| 693 | + content[0] = content[0].slice(Math.max(0, sourceColumn - 1)); |
| 694 | + content = content.map( |
| 695 | + l => |
| 696 | + l |
| 697 | + .trim() // Remove whitespace |
| 698 | + .replace(/\s*\/\/.*$(?<!,)/u, ""), // and trailing in-line comments that aren't part of the test `code` |
| 699 | + ); |
| 700 | + |
| 701 | + // Roots |
| 702 | + const validStartIndex = content.findIndex(line => |
| 703 | + /\bvalid:/u.test(line), |
| 704 | + ); |
| 705 | + const invalidStartIndex = content.findIndex(line => |
| 706 | + /\binvalid:/u.test(line), |
| 707 | + ); |
| 708 | + |
| 709 | + testLocations.valid = `${sourceFile}:${ |
| 710 | + sourceLine + validStartIndex |
| 711 | + }`; |
| 712 | + testLocations.invalid = `${sourceFile}:${ |
| 713 | + sourceLine + invalidStartIndex |
| 714 | + }`; |
| 715 | + |
| 716 | + // Scenario basics |
| 717 | + const validEndIndex = |
| 718 | + validStartIndex < invalidStartIndex |
| 719 | + ? invalidStartIndex |
| 720 | + : content.length; |
| 721 | + const invalidEndIndex = |
| 722 | + validStartIndex < invalidStartIndex |
| 723 | + ? content.length |
| 724 | + : validStartIndex; |
| 725 | + |
| 726 | + const validLines = content.slice( |
| 727 | + validStartIndex, |
| 728 | + validEndIndex, |
| 729 | + ); |
| 730 | + const invalidLines = content.slice( |
| 731 | + invalidStartIndex, |
| 732 | + invalidEndIndex, |
| 733 | + ); |
| 734 | + |
| 735 | + let objectDepth = 0; |
| 736 | + const validLineIndexes = validLines |
| 737 | + .map((l, i) => { |
| 738 | + // matches `key: {` and `{` |
| 739 | + if (/^(?:\w+\s*:\s*)?\{/u.test(l)) { |
| 740 | + objectDepth++; |
| 741 | + } |
| 742 | + |
| 743 | + if (objectDepth > 0) { |
| 744 | + if (l.endsWith("}") || l.endsWith("},")) { |
| 745 | + objectDepth--; |
| 746 | + } |
| 747 | + |
| 748 | + return objectDepth <= 1 && l.includes("code:") |
| 749 | + ? i |
| 750 | + : null; |
| 751 | + } |
| 752 | + |
| 753 | + return l.endsWith(",") ? i : null; |
| 754 | + }) |
| 755 | + .filter(Boolean); |
| 756 | + const invalidLineIndexes = invalidLines |
| 757 | + .map((l, i) => |
| 758 | + l.trimStart().startsWith("errors:") ? i : null, |
| 759 | + ) |
| 760 | + .filter(Boolean); |
| 761 | + |
| 762 | + Object.assign( |
| 763 | + testLocations, |
| 764 | + { |
| 765 | + [`valid[0]`]: `${sourceFile}:${ |
| 766 | + sourceLine + validStartIndex |
| 767 | + }`, |
| 768 | + }, |
| 769 | + Object.fromEntries( |
| 770 | + validLineIndexes.map((location, validIndex) => [ |
| 771 | + `valid[${validIndex}]`, |
| 772 | + `${sourceFile}:${ |
| 773 | + sourceLine + validStartIndex + location |
| 774 | + }`, |
| 775 | + ]), |
| 776 | + ), |
| 777 | + Object.fromEntries( |
| 778 | + invalidLineIndexes.map((location, invalidIndex) => [ |
| 779 | + `invalid[${invalidIndex}]`, |
| 780 | + `${sourceFile}:${ |
| 781 | + sourceLine + invalidStartIndex + location |
| 782 | + }`, |
| 783 | + ]), |
| 784 | + ), |
| 785 | + ); |
| 786 | + |
| 787 | + // Indexes for errors inside each invalid test case |
| 788 | + invalidLineIndexes.push(invalidLines.length); |
| 789 | + |
| 790 | + for (let i = 0; i < invalidLineIndexes.length - 1; i++) { |
| 791 | + const start = invalidLineIndexes[i]; |
| 792 | + const end = invalidLineIndexes[i + 1]; |
| 793 | + const errorLines = invalidLines.slice(start, end); |
| 794 | + let errorObjectDepth = 0; |
| 795 | + const errorLineIndexes = errorLines |
| 796 | + .map((l, j) => { |
| 797 | + if (l.startsWith("{") || l.endsWith("{")) { |
| 798 | + errorObjectDepth++; |
| 799 | + |
| 800 | + if (l.endsWith("}") || l.endsWith("},")) { |
| 801 | + errorObjectDepth--; |
| 802 | + } |
| 803 | + |
| 804 | + return errorObjectDepth <= 1 ? j : null; |
| 805 | + } |
| 806 | + |
| 807 | + if (errorObjectDepth > 0) { |
| 808 | + if (l.endsWith("}") || l.endsWith("},")) { |
| 809 | + errorObjectDepth--; |
| 810 | + } |
| 811 | + |
| 812 | + return null; |
| 813 | + } |
| 814 | + |
| 815 | + return l.endsWith(",") ? j : null; |
| 816 | + }) |
| 817 | + .filter(Boolean); |
| 818 | + |
| 819 | + Object.assign( |
| 820 | + testLocations, |
| 821 | + Object.fromEntries( |
| 822 | + errorLineIndexes.map((line, errorIndex) => [ |
| 823 | + `invalid[${i}].errors[${errorIndex}]`, |
| 824 | + `${sourceFile}:${ |
| 825 | + sourceLine + |
| 826 | + invalidStartIndex + |
| 827 | + start + |
| 828 | + line |
| 829 | + }`, |
| 830 | + ]), |
| 831 | + ), |
| 832 | + ); |
| 833 | + } |
| 834 | + } |
| 835 | + } |
| 836 | + |
| 837 | + return testLocations[key] || "unknown source"; |
| 838 | + }; |
| 839 | +} |
| 840 | + |
669 | 841 | //------------------------------------------------------------------------------ |
670 | 842 | // Public Interface |
671 | 843 | //------------------------------------------------------------------------------ |
@@ -866,7 +1038,7 @@ class RuleTester { |
866 | 1038 | assertRule(rule, ruleName); |
867 | 1039 | assertTest(test, ruleName); |
868 | 1040 |
|
869 | | - const invocationLocation = getInvocationLocation(this.run); |
| 1041 | + const estimateTestLocation = buildLazyTestLocationEstimator(this.run); |
870 | 1042 |
|
871 | 1043 | const baseConfig = [ |
872 | 1044 | { |
@@ -1744,9 +1916,9 @@ class RuleTester { |
1744 | 1916 | error.stack = error.stack.replace( |
1745 | 1917 | /^ +at /mu, |
1746 | 1918 | [ |
1747 | | - ` at RuleTester.run.valid[${index}]`, |
1748 | | - ` at RuleTester.run.valid`, |
1749 | | - ` at RuleTester.run (${invocationLocation})`, |
| 1919 | + ` roughly at RuleTester.run.valid[${index}] (${estimateTestLocation(`valid[${index}]`)})`, |
| 1920 | + ` roughly at RuleTester.run.valid (${estimateTestLocation("valid")})`, |
| 1921 | + ` at RuleTester.run (${estimateTestLocation("root")})`, |
1750 | 1922 | " at ", |
1751 | 1923 | ].join("\n"), |
1752 | 1924 | ); |
@@ -1789,12 +1961,12 @@ class RuleTester { |
1789 | 1961 | ...(typeof errorIndex === |
1790 | 1962 | "number" |
1791 | 1963 | ? [ |
1792 | | - ` at RuleTester.run.invalid[${index}].error[${errorIndex}]`, |
| 1964 | + ` roughly at RuleTester.run.invalid[${index}].error[${errorIndex}] (${estimateTestLocation(`invalid[${index}].errors[${errorIndex}]`)})`, |
1793 | 1965 | ] |
1794 | 1966 | : []), |
1795 | | - ` at RuleTester.run.invalid[${index}]`, |
1796 | | - ` at RuleTester.run.invalid`, |
1797 | | - ` at RuleTester.run (${invocationLocation})`, |
| 1967 | + ` roughly at RuleTester.run.invalid[${index}] (${estimateTestLocation(`invalid[${index}]`)})`, |
| 1968 | + ` roughly at RuleTester.run.invalid (${estimateTestLocation("invalid")})`, |
| 1969 | + ` at RuleTester.run (${estimateTestLocation("root")})`, |
1798 | 1970 | " at ", |
1799 | 1971 | ].join("\n"), |
1800 | 1972 | ); |
|
0 commit comments