diff --git a/packages/hardhat-typechain/.prettierignore b/packages/hardhat-typechain/.prettierignore index f4ff47bd3eb..cf41b63d6de 100644 --- a/packages/hardhat-typechain/.prettierignore +++ b/packages/hardhat-typechain/.prettierignore @@ -4,5 +4,5 @@ CHANGELOG.md /test/fixture-projects/**/artifacts /test/fixture-projects/**/cache +/test/fixture-projects/**/types /test/fixture-projects/custom-out-dir/custom-types -test/fixture-projects/generate-types/types diff --git a/packages/hardhat-typechain/src/internal/hook-handlers/solidity.ts b/packages/hardhat-typechain/src/internal/hook-handlers/solidity.ts index 86da136dcd9..9c99fd7cffd 100644 --- a/packages/hardhat-typechain/src/internal/hook-handlers/solidity.ts +++ b/packages/hardhat-typechain/src/internal/hook-handlers/solidity.ts @@ -1,10 +1,13 @@ import type { HookContext, SolidityHooks } from "hardhat/types/hooks"; import type { BuildOptions, + BuildScope, CompilationJobCreationError, FileBuildResult, } from "hardhat/types/solidity"; +import path from "node:path"; + import { generateTypes } from "../generate-types.js"; export default async (): Promise> => { @@ -34,14 +37,25 @@ export default async (): Promise> => { // Clear cache to ensure fresh data after compilation await context.artifacts.clearCache(); - // Get all artifact paths and generate types - const allArtifactPaths = await context.artifacts.getAllArtifactPaths(); + let artifactPaths: string[]; + + if (context.config.solidity.splitTestsCompilation) { + artifactPaths = Array.from( + await context.artifacts.getAllArtifactPaths(), + ); + } else { + // Contracts and tests share the artifacts folder. + // Filter out test artifacts using each artifact's sourceName (derived + // from its fully qualified name), which is the project-relative or npm + // source identifier. + artifactPaths = await getContractArtifactPaths(context); + } await generateTypes( context.config.paths.root, context.config.typechain, context.globalOptions.noTypechain, - Array.from(allArtifactPaths), + artifactPaths, ); return result; @@ -50,3 +64,47 @@ export default async (): Promise> => { return handlers; }; + +async function getContractArtifactPaths( + context: HookContext, +): Promise { + const fqns = await context.artifacts.getAllFullyQualifiedNames(); + const projectRoot = context.config.paths.root; + + const scopeBySource = new Map(); + const contractFqns: string[] = []; + + for (const fqn of fqns) { + const sourceName = fqn.slice(0, fqn.lastIndexOf(":")); + + let scope = scopeBySource.get(sourceName); + if (scope === undefined) { + const fsPath = path.resolve(projectRoot, sourceName); + + // npm files will be classified as "contracts" because their sourceName is + // not an existing file, and "contracts" is the default. + // + // If the package name clashed with + // ```ts + // path.relative( + // context.config.paths.root, + // context.config.paths.tests.solidity + // ) + // ``` + // + // They could be misclassified as test files. This is highly improbable, + // so we don't check it. You could read the artifact and see if the + // inputSourceName starts with `npm/` to rule this out. + scope = await context.solidity.getScope(fsPath); + scopeBySource.set(sourceName, scope); + } + + if (scope === "contracts") { + contractFqns.push(fqn); + } + } + + return Promise.all( + contractFqns.map((fqn) => context.artifacts.getArtifactPath(fqn)), + ); +} diff --git a/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/contracts/A.sol b/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/contracts/A.sol new file mode 100644 index 00000000000..7b6c923235f --- /dev/null +++ b/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/contracts/A.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract A { + function getMessage() external pure returns (string memory) { + return "Hello from A contract!"; + } +} diff --git a/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/hardhat.config.ts b/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/hardhat.config.ts new file mode 100644 index 00000000000..ff8b4c56321 --- /dev/null +++ b/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/hardhat.config.ts @@ -0,0 +1 @@ +export default {}; diff --git a/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/node_modules/@fake/lib/Token.sol b/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/node_modules/@fake/lib/Token.sol new file mode 100644 index 00000000000..34037a91695 --- /dev/null +++ b/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/node_modules/@fake/lib/Token.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract Token { + function name() external pure returns (string memory) { + return "FakeToken"; + } +} diff --git a/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/node_modules/@fake/lib/package.json b/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/node_modules/@fake/lib/package.json new file mode 100644 index 00000000000..24b444ac72a --- /dev/null +++ b/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/node_modules/@fake/lib/package.json @@ -0,0 +1,4 @@ +{ + "name": "@fake/lib", + "version": "1.0.0" +} \ No newline at end of file diff --git a/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/node_modules/test-lib/Helper.sol b/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/node_modules/test-lib/Helper.sol new file mode 100644 index 00000000000..f43850dd846 --- /dev/null +++ b/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/node_modules/test-lib/Helper.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract Helper { + function ping() external pure returns (string memory) { + return "pong"; + } +} diff --git a/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/node_modules/test-lib/package.json b/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/node_modules/test-lib/package.json new file mode 100644 index 00000000000..aace75bf6b3 --- /dev/null +++ b/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/node_modules/test-lib/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-lib", + "version": "1.0.0" +} diff --git a/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/package.json b/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/package.json new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/packages/hardhat-typechain/test/fixture-projects/unified-mode-npm/package.json @@ -0,0 +1 @@ +{} diff --git a/packages/hardhat-typechain/test/fixture-projects/unified-mode/contracts/A.sol b/packages/hardhat-typechain/test/fixture-projects/unified-mode/contracts/A.sol new file mode 100644 index 00000000000..7b6c923235f --- /dev/null +++ b/packages/hardhat-typechain/test/fixture-projects/unified-mode/contracts/A.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract A { + function getMessage() external pure returns (string memory) { + return "Hello from A contract!"; + } +} diff --git a/packages/hardhat-typechain/test/fixture-projects/unified-mode/contracts/A.t.sol b/packages/hardhat-typechain/test/fixture-projects/unified-mode/contracts/A.t.sol new file mode 100644 index 00000000000..dcbac4fed0f --- /dev/null +++ b/packages/hardhat-typechain/test/fixture-projects/unified-mode/contracts/A.t.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {A} from "./A.sol"; + +contract ATest { + A a; + + function setUp() public { + a = new A(); + } + + function test_Assertion() public view { + require(1 == 1, "test assertion"); + } +} diff --git a/packages/hardhat-typechain/test/fixture-projects/unified-mode/hardhat.config.ts b/packages/hardhat-typechain/test/fixture-projects/unified-mode/hardhat.config.ts new file mode 100644 index 00000000000..162f9153559 --- /dev/null +++ b/packages/hardhat-typechain/test/fixture-projects/unified-mode/hardhat.config.ts @@ -0,0 +1,14 @@ +import type { HardhatUserConfig } from "hardhat/config"; + +// eslint-disable-next-line import/no-relative-packages -- allow in fixture projects +import hardhatTypechain from "../../../src/index.js"; + +const config: HardhatUserConfig = { + solidity: { + version: "0.8.28", + splitTestsCompilation: false, + }, + plugins: [hardhatTypechain], +}; + +export default config; diff --git a/packages/hardhat-typechain/test/fixture-projects/unified-mode/package.json b/packages/hardhat-typechain/test/fixture-projects/unified-mode/package.json new file mode 100644 index 00000000000..d9a203d79eb --- /dev/null +++ b/packages/hardhat-typechain/test/fixture-projects/unified-mode/package.json @@ -0,0 +1,5 @@ +{ + "name": "hardhat-project", + "private": true, + "type": "module" +} diff --git a/packages/hardhat-typechain/test/fixture-projects/unified-mode/test/BTest.sol b/packages/hardhat-typechain/test/fixture-projects/unified-mode/test/BTest.sol new file mode 100644 index 00000000000..3f5a4367051 --- /dev/null +++ b/packages/hardhat-typechain/test/fixture-projects/unified-mode/test/BTest.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract BTest { + function test_Assertion() public view { + require(1 == 1, "test assertion"); + } +} diff --git a/packages/hardhat-typechain/test/index.ts b/packages/hardhat-typechain/test/index.ts index e1983075a45..b31c43bbd0d 100644 --- a/packages/hardhat-typechain/test/index.ts +++ b/packages/hardhat-typechain/test/index.ts @@ -355,7 +355,14 @@ describe("hardhat-typechain", () => { `./fixture-projects/${projectFolder}/hardhat.config.js` ); - const hre = await createHardhatRuntimeEnvironment(hardhatConfig.default); + // scope: "tests" requires splitTestsCompilation: true + const hre = await createHardhatRuntimeEnvironment({ + ...hardhatConfig.default, + solidity: { + ...hardhatConfig.default.solidity, + splitTestsCompilation: true, + }, + }); await hre.tasks.getTask("clean").run(); @@ -371,6 +378,156 @@ describe("hardhat-typechain", () => { }); }); + describe("types are not generated for test artifacts when splitTestsCompilation is false", () => { + const projectFolder = "unified-mode"; + + useFixtureProject(projectFolder); + + before(async () => { + await remove(`${process.cwd()}/types`); + + const hardhatConfig = await import( + `./fixture-projects/${projectFolder}/hardhat.config.js` + ); + + const hre = await createHardhatRuntimeEnvironment(hardhatConfig.default); + + await hre.tasks.getTask("clean").run(); + await hre.tasks.getTask("build").run(); + }); + + it("should generate types for contract artifacts", async () => { + assert.equal(await exists(`${process.cwd()}/types`), true); + + // Contract A should have types generated + assert.equal( + await exists( + path.join(process.cwd(), "types", "ethers-contracts", "A.ts"), + ), + true, + ); + }); + + it("should not generate types for .t.sol test artifacts", async () => { + // ATest from contracts/A.t.sol should NOT have types + assert.equal( + await exists( + path.join(process.cwd(), "types", "ethers-contracts", "ATest.ts"), + ), + false, + ); + }); + + it("should not generate types for test directory artifacts", async () => { + // BTest from test/BTest.sol should NOT have types + assert.equal( + await exists( + path.join(process.cwd(), "types", "ethers-contracts", "BTest.ts"), + ), + false, + ); + }); + + it("should classify artifacts using getScope, not artifact-path heuristics", async () => { + // The A.t.sol test file imports A.sol, so A.sol's contract artifact + // appears under contracts/A.sol/ (a contract path). The filter must + // use getScope() on the source file, not a path-based heuristic. + // A.sol is a contract, so its type should be generated. + assert.equal( + await exists( + path.join( + process.cwd(), + "types", + "ethers-contracts", + "factories", + "A__factory.ts", + ), + ), + true, + ); + + // ATest is a test (from .t.sol), so its factory should NOT exist + assert.equal( + await exists( + path.join( + process.cwd(), + "types", + "ethers-contracts", + "factories", + "ATest__factory.ts", + ), + ), + false, + ); + }); + }); + + describe("npm-dependency artifacts are classified as contracts in unified mode", () => { + useFixtureProject("unified-mode-npm"); + + before(async () => { + await remove(`${process.cwd()}/types`); + + const hre = await createHardhatRuntimeEnvironment({ + solidity: { + version: "0.8.28", + splitTestsCompilation: false, + npmFilesToBuild: ["@fake/lib/Token.sol", "test-lib/Helper.sol"], + }, + plugins: [hardhatTypechain], + }); + + await hre.tasks.getTask("clean").run(); + await hre.tasks.getTask("build").run(); + }); + + it("should generate types for the npm-dependency artifact", async () => { + assert.equal( + await exists( + path.join( + process.cwd(), + "types", + "ethers-contracts", + "@fake", + "lib", + "Token.ts", + ), + ), + true, + ); + }); + + it("should generate types for an unscoped npm package whose name starts with the tests dir name", async () => { + assert.equal( + await exists( + path.join( + process.cwd(), + "types", + "ethers-contracts", + "test-lib", + "Helper.ts", + ), + ), + true, + ); + }); + + it("should generate types for the local contract artifact", async () => { + assert.equal( + await exists( + path.join( + process.cwd(), + "types", + "ethers-contracts", + "contracts", + "A.ts", + ), + ), + true, + ); + }); + }); + describe("clean hook removes the types folder", () => { describe("with default outDir", () => { const projectFolder = "generate-types"; diff --git a/packages/hardhat/src/internal/builtin-plugins/artifacts/artifact-manager.ts b/packages/hardhat/src/internal/builtin-plugins/artifacts/artifact-manager.ts index c13f5d46475..f27bfca4a53 100644 --- a/packages/hardhat/src/internal/builtin-plugins/artifacts/artifact-manager.ts +++ b/packages/hardhat/src/internal/builtin-plugins/artifacts/artifact-manager.ts @@ -191,7 +191,7 @@ export class ArtifactManagerImplementation implements ArtifactManager { HardhatError.ERRORS.CORE.ARTIFACTS.MULTIPLE_FOUND, { contractName: contractNameOrFullyQualifiedName, - candidates: Array.from(fqns).join(EOL), + candidates: Array.from(fqns).sort().join(EOL), }, ); } diff --git a/packages/hardhat/src/internal/builtin-plugins/solidity/build-system/build-system.ts b/packages/hardhat/src/internal/builtin-plugins/solidity/build-system/build-system.ts index 08e238d3f2a..c04662110e4 100644 --- a/packages/hardhat/src/internal/builtin-plugins/solidity/build-system/build-system.ts +++ b/packages/hardhat/src/internal/builtin-plugins/solidity/build-system/build-system.ts @@ -155,7 +155,7 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { public async getScope(fsPath: string): Promise { if ( - fsPath.startsWith(this.#options.solidityTestsPath) && + fsPath.startsWith(this.#options.solidityTestsPath + path.sep) && fsPath.endsWith(".sol") ) { return "tests"; @@ -163,7 +163,7 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { if (fsPath.endsWith(".t.sol")) { for (const sourcesPath of this.#options.soliditySourcesPaths) { - if (fsPath.startsWith(sourcesPath)) { + if (fsPath.startsWith(sourcesPath + path.sep)) { return "tests"; } } @@ -1122,7 +1122,7 @@ export class SolidityBuildSystemImplementation implements SolidityBuildSystem { // Get all the reachable build info files const buildInfoFiles = await getAllFilesMatching(buildInfosDir, (f) => - f.startsWith(buildInfosDir), + f.startsWith(buildInfosDir + path.sep), ); for (const buildInfoFile of buildInfoFiles) { diff --git a/packages/hardhat/test/internal/builtin-plugins/solidity/build-system/build-system.ts b/packages/hardhat/test/internal/builtin-plugins/solidity/build-system/build-system.ts index a007af44c35..a611b7fe9ca 100644 --- a/packages/hardhat/test/internal/builtin-plugins/solidity/build-system/build-system.ts +++ b/packages/hardhat/test/internal/builtin-plugins/solidity/build-system/build-system.ts @@ -410,3 +410,105 @@ describe( }); }, ); + +describe("SolidityBuildSystemImplementation.getScope", () => { + const projectRoot = path.join(path.sep, "project"); + const solidityTestsPath = path.join(projectRoot, "tests"); + const soliditySourcesPaths = [path.join(projectRoot, "contracts")]; + + function makeBuildSystem(): SolidityBuildSystemImplementation { + const hooks = new HookManagerImplementation(projectRoot, []); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- hooks context is irrelevant for getScope + hooks.setContext({} as HookContext); + const solidityConfig: SolidityConfig = { + profiles: { + default: { + compilers: [], + overrides: {}, + isolated: false, + preferWasm: false, + }, + }, + npmFilesToBuild: [], + registeredCompilerTypes: ["solc"], + splitTestsCompilation: false, + }; + return new SolidityBuildSystemImplementation(hooks, { + solidityConfig, + projectRoot, + soliditySourcesPaths, + artifactsPath: path.join(projectRoot, "artifacts"), + cachePath: path.join(projectRoot, "cache"), + solidityTestsPath, + }); + } + + const solidity = makeBuildSystem(); + + it("returns 'tests' for a .sol file directly inside solidityTestsPath", async () => { + assert.equal( + await solidity.getScope(path.join(solidityTestsPath, "Foo.sol")), + "tests", + ); + }); + + it("returns 'tests' for a .sol file nested inside solidityTestsPath", async () => { + assert.equal( + await solidity.getScope(path.join(solidityTestsPath, "sub", "Foo.sol")), + "tests", + ); + }); + + it("returns 'contracts' for a file in a directory whose name is a prefix of solidityTestsPath", async () => { + assert.equal( + await solidity.getScope(path.join(projectRoot, "tests-extra", "Foo.sol")), + "contracts", + ); + }); + + it("returns 'contracts' for a plain .sol file inside a sources path", async () => { + assert.equal( + await solidity.getScope(path.join(soliditySourcesPaths[0], "Foo.sol")), + "contracts", + ); + }); + + it("returns 'tests' for a .t.sol file inside a sources path", async () => { + assert.equal( + await solidity.getScope(path.join(soliditySourcesPaths[0], "Foo.t.sol")), + "tests", + ); + }); + + it("returns 'tests' for a .t.sol file nested inside a sources path", async () => { + assert.equal( + await solidity.getScope( + path.join(soliditySourcesPaths[0], "sub", "Foo.t.sol"), + ), + "tests", + ); + }); + + it("returns 'contracts' for a .t.sol file in a directory whose name is a prefix of a sources path", async () => { + assert.equal( + await solidity.getScope( + path.join(projectRoot, "contracts-extra", "Foo.t.sol"), + ), + "contracts", + ); + }); + + it("returns 'contracts' for a .t.sol file outside any sources path", async () => { + assert.equal( + await solidity.getScope(path.join(projectRoot, "elsewhere", "Foo.t.sol")), + "contracts", + ); + }); + + it("returns 'contracts' for a .sol file outside any sources or tests path", async () => { + assert.equal( + await solidity.getScope(path.join(projectRoot, "elsewhere", "Foo.sol")), + "contracts", + ); + }); +}); diff --git a/packages/hardhat/test/internal/builtin-plugins/solidity/build-system/integration/artifacts.ts b/packages/hardhat/test/internal/builtin-plugins/solidity/build-system/integration/artifacts.ts new file mode 100644 index 00000000000..dc62a609fd3 --- /dev/null +++ b/packages/hardhat/test/internal/builtin-plugins/solidity/build-system/integration/artifacts.ts @@ -0,0 +1,159 @@ +import assert from "node:assert/strict"; +import { EOL } from "node:os"; +import { describe, it } from "node:test"; + +import { HardhatError } from "@nomicfoundation/hardhat-errors"; +import { assertRejectsWithHardhatError } from "@nomicfoundation/hardhat-test-utils"; + +import { useTestProjectTemplate } from "../resolver/helpers.js"; + +const basicProjectTemplate = { + name: "test", + version: "1.0.0", + files: { + "contracts/Foo.sol": `// SPDX-License-Identifier: UNLICENSED \n pragma solidity ^0.8.0; contract Foo {}`, + "contracts/Foo.t.sol": ` + // SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.0; + + import {Foo} from "./Foo.sol"; + + contract FooTest { + Foo foo; + + function setUp() public { + foo = new Foo(); + } + + function test_Assertion() public view { + require(1 == 1, "test assertion"); + } + } + `, + "test/OtherFooTest.sol": ` + // SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.0; + + import {Foo} from "../contracts/Foo.sol"; + + contract OtherFooTest { + Foo foo; + + function setUp() public { + foo = new Foo(); + } + + function test_Assertion() public view { + require(1 == 1, "test assertion"); + } + } + `, + }, +}; + +const unifiedTestsCompilationConfig = { + solidity: { + version: "0.8.28", + splitTestsCompilation: false, + }, +}; + +describe("artifact API in unified mode", function () { + it("getAllArtifactPaths includes test artifacts", async () => { + await using project = await useTestProjectTemplate(basicProjectTemplate); + const hre = await project.getHRE(unifiedTestsCompilationConfig); + + await hre.tasks.getTask("build").run(); + + const allPaths = await hre.artifacts.getAllArtifactPaths(); + const pathsArray = Array.from(allPaths); + + assert.ok( + pathsArray.some((p) => p.includes("Foo.sol") && !p.includes(".t.sol")), + "Expected contract artifact path in getAllArtifactPaths", + ); + assert.ok( + pathsArray.some((p) => p.includes("Foo.t.sol")), + "Expected test artifact path (Foo.t.sol) in getAllArtifactPaths", + ); + assert.ok( + pathsArray.some((p) => p.includes("OtherFooTest.sol")), + "Expected test artifact path (OtherFooTest.sol) in getAllArtifactPaths", + ); + }); + + it("getAllFullyQualifiedNames includes test artifacts", async () => { + await using project = await useTestProjectTemplate(basicProjectTemplate); + const hre = await project.getHRE(unifiedTestsCompilationConfig); + + await hre.tasks.getTask("build").run(); + + const allNames = await hre.artifacts.getAllFullyQualifiedNames(); + + assert.ok( + allNames.has("contracts/Foo.sol:Foo"), + "Expected contract FQN in getAllFullyQualifiedNames", + ); + assert.ok( + allNames.has("contracts/Foo.t.sol:FooTest"), + "Expected test FQN (FooTest) in getAllFullyQualifiedNames", + ); + assert.ok( + allNames.has("test/OtherFooTest.sol:OtherFooTest"), + "Expected test FQN (OtherFooTest) in getAllFullyQualifiedNames", + ); + }); + + it("bare-name lookup becomes ambiguous when a test and contract share a name", async () => { + const duplicateNameTemplate = { + name: "test", + version: "1.0.0", + files: { + "contracts/Foo.sol": `// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.0;\ncontract Foo {}`, + "test/Foo.sol": `// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.0;\ncontract Foo {}`, + }, + }; + + await using project = await useTestProjectTemplate(duplicateNameTemplate); + const hre = await project.getHRE(unifiedTestsCompilationConfig); + + await hre.tasks.getTask("build").run(); + await hre.artifacts.clearCache(); + + // Bare-name lookup should throw because both contract and test + // produce artifacts named "Foo" + await assertRejectsWithHardhatError( + hre.artifacts.readArtifact("Foo"), + HardhatError.ERRORS.CORE.ARTIFACTS.MULTIPLE_FOUND, + { + contractName: "Foo", + candidates: `contracts/Foo.sol:Foo${EOL}test/Foo.sol:Foo`, + }, + ); + }); + + it("fully qualified name lookup still works when a test and contract share a name", async () => { + const duplicateNameTemplate = { + name: "test", + version: "1.0.0", + files: { + "contracts/Foo.sol": `// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.0;\ncontract Foo {}`, + "test/Foo.sol": `// SPDX-License-Identifier: UNLICENSED\npragma solidity ^0.8.0;\ncontract Foo {}`, + }, + }; + + await using project = await useTestProjectTemplate(duplicateNameTemplate); + const hre = await project.getHRE(unifiedTestsCompilationConfig); + + await hre.tasks.getTask("build").run(); + await hre.artifacts.clearCache(); + + const contractArtifact = await hre.artifacts.readArtifact( + "contracts/Foo.sol:Foo", + ); + assert.equal(contractArtifact.contractName, "Foo"); + + const testArtifact = await hre.artifacts.readArtifact("test/Foo.sol:Foo"); + assert.equal(testArtifact.contractName, "Foo"); + }); +});