From b21c82e76ae53520a2f9bb439188c310db5c9992 Mon Sep 17 00:00:00 2001 From: djviau Date: Thu, 28 Sep 2023 09:51:48 -0400 Subject: [PATCH 1/2] fix ffi tests --- README.md | 2 +- test-ffi/onchain/svg.t.sol | 41 +++++----- test-ffi/reference/ExampleNFT.t.sol | 48 ++++++----- test-ffi/scripts/process_json.js | 120 +++++++++++++++------------- 4 files changed, 113 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 4d5428d..1195353 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Both the local profile and the CI profile for the ffi tests use a low number of ## Expected local behavior -The `ExampleNFT.t.sol` file will call `ExampleNFT.sol`'s `tokenURI` function, decode the base64 encoded response, write the decoded version to `./test-ffi/tmp/temp.json`, and then call the `process_json.js` file a few times to get string values. If the expected values and the actual values match, the test will pass. A `temp.json` file will be left behind. You can ignore it or delete it; Forge makes a new one on the fly if it's not there. And it's ignored in the `.gitignore` file, so there's no need to worry about pushing cruft or top secret metadata to a shared/public repo. The tests in `svg.t.sol` behave more or less the same way, except that they'll produce many more temporary files. +The `ExampleNFT.t.sol` file will call `ExampleNFT.sol`'s `tokenURI` function, decode the base64 encoded response, write the decoded version to `./test-ffi/tmp/temp.json`, and then call the `process_json.js` file a few times to get string values. If the expected values and the actual values match, the test will pass and the files will be cleaned up. If they fail, a `temp-*.json` file will be left behind for reference. You can ignore it or delete it after you're done inspecting it. Forge makes a new one on the fly if it's not there. And it's ignored in the `.gitignore` file, so there's no need to worry about pushing cruft or top secret metadata to a shared/public repo. The tests in `svg.t.sol` behave more or less the same way, except that they'll produce many more temporary files. ## Expected CI behavior diff --git a/test-ffi/onchain/svg.t.sol b/test-ffi/onchain/svg.t.sol index 64886f8..37889ad 100644 --- a/test-ffi/onchain/svg.t.sol +++ b/test-ffi/onchain/svg.t.sol @@ -7,8 +7,6 @@ import {svg} from "../../src/onchain/svg.sol"; import {ExampleNFT} from "src/reference/ExampleNFT.sol"; import {svg} from "../../src/onchain/svg.sol"; -import "forge-std/console.sol"; - contract svgTest is Test { ExampleNFT testExampleNft; @@ -206,39 +204,27 @@ contract svgTest is Test { } function _validateSvg(string memory fileName) internal { + string memory filePath = string(abi.encodePacked(TEMP_SVG_DIR_PATH_AND_PREFIX, fileName, TEMP_SVG_FILE_TYPE)); + // Run the validate_svg.js script on the file to validate the svg. string[] memory commandLineInputs = new string[](3); commandLineInputs[0] = "node"; commandLineInputs[1] = VALIDATE_SVG_PATH; - commandLineInputs[2] = string(abi.encodePacked(TEMP_SVG_DIR_PATH_AND_PREFIX, fileName, TEMP_SVG_FILE_TYPE)); + commandLineInputs[2] = filePath; (bool isValid, string memory svg) = abi.decode(vm.ffi(commandLineInputs), (bool, string)); assertEq(isValid, true, string(abi.encodePacked("The svg should be valid. Invalid svg: ", svg))); + + _cleanUp(filePath); } function _populateTempFileWithJson(uint256 tokenId) internal { // Get the raw URI response. string memory rawUri = testExampleNft.tokenURI(tokenId); - // Remove the data:application/json;base64, prefix. - string memory uri = _cleanedUri(rawUri); - // Decode the base64 encoded json. - bytes memory decoded = Base64.decode(uri); // Write the decoded json to a file. - vm.writeFile(TEMP_JSON_PATH, string(decoded)); - } - - function _cleanedUri(string memory uri) internal pure returns (string memory) { - uint256 stringLength; - - // Get the length of the string from the abi encoded version. - assembly { - stringLength := mload(uri) - } - - // Remove the data:application/json;base64, prefix. - return _substring(uri, 29, stringLength); + vm.writeFile(TEMP_JSON_PATH, rawUri); } function _getImage() internal returns (string memory) { @@ -258,7 +244,9 @@ contract svgTest is Test { (,, string memory image) = abi.decode(vm.ffi(commandLineInputs), (string, string, string)); - return _cleanedSvg(image); + _cleanUp(TEMP_JSON_PATH); + + return string(Base64.decode(_cleanedSvg(image))); } function _cleanedSvg(string memory uri) internal pure returns (string memory) { @@ -269,8 +257,8 @@ contract svgTest is Test { stringLength := mload(uri) } - // Remove the "data:image/svg+xml;" prefix. - return _substring(uri, 19, stringLength); + // Remove the "data:image/svg+xml;base64," prefix. + return _substring(uri, 26, stringLength); } function _substring(string memory str, uint256 startIndex, uint256 endIndex) public pure returns (string memory) { @@ -282,4 +270,11 @@ contract svgTest is Test { } return string(result); } + + function _cleanUp(string memory file) internal { + if (vm.exists(file)) { + vm.removeFile(file); + } + assertFalse(vm.exists(file)); + } } diff --git a/test-ffi/reference/ExampleNFT.t.sol b/test-ffi/reference/ExampleNFT.t.sol index e0ef8f7..7b72fa0 100644 --- a/test-ffi/reference/ExampleNFT.t.sol +++ b/test-ffi/reference/ExampleNFT.t.sol @@ -51,30 +51,16 @@ contract ExampleNFTTest is Test { _generateError(tokenId, i, "displayType inconsistent with expected") ); } + + _cleanUp(TEMP_JSON_PATH); } function _populateTempFileWithJson(uint256 tokenId) internal { // Get the raw URI response. string memory rawUri = testExampleNft.tokenURI(tokenId); - // Remove the data:application/json;base64, prefix. - string memory uri = _cleanedUri(rawUri); - // Decode the base64 encoded json. - bytes memory decoded = Base64.decode(uri); // Write the decoded json to a file. - vm.writeFile(TEMP_JSON_PATH, string(decoded)); - } - - function _cleanedUri(string memory uri) internal pure returns (string memory) { - uint256 stringLength; - - // Get the length of the string from the abi encoded version. - assembly { - stringLength := mload(uri) - } - - // Remove the data:application/json;base64, prefix. - return _substring(uri, 29, stringLength); + vm.writeFile(TEMP_JSON_PATH, rawUri); } function _substring(string memory str, uint256 startIndex, uint256 endIndex) public pure returns (string memory) { @@ -106,6 +92,20 @@ contract ExampleNFTTest is Test { commandLineInputs[3] = "--top-level"; (name, description, image) = abi.decode(vm.ffi(commandLineInputs), (string, string, string)); + + image = string(Base64.decode(_cleanedSvg(image))); + } + + function _cleanedSvg(string memory uri) internal pure returns (string memory) { + uint256 stringLength; + + // Get the length of the string from the abi encoded version. + assembly { + stringLength := mload(uri) + } + + // Remove the "data:image/svg+xml;base64," prefix. + return _substring(uri, 26, stringLength); } function _getAttributeAtIndex(uint256 attributeIndex) @@ -137,11 +137,16 @@ contract ExampleNFTTest is Test { function _generateExpectedTokenImage(uint256 tokenId) internal pure returns (string memory) { return string( abi.encodePacked( - 'data:image/svg+xml;', + "", vm.toString(tokenId), "" ) ); + // abi.encodePacked( + // 'data:image/svg+xml;', + // vm.toString(tokenId), + // "" + // ) } function _generateError(uint256 tokenId, uint256 traitIndex, string memory message) @@ -155,4 +160,11 @@ contract ExampleNFTTest is Test { ) ); } + + function _cleanUp(string memory file) internal { + if (vm.exists(file)) { + vm.removeFile(file); + } + assertFalse(vm.exists(file)); + } } diff --git a/test-ffi/scripts/process_json.js b/test-ffi/scripts/process_json.js index 23351b3..bf8fab0 100644 --- a/test-ffi/scripts/process_json.js +++ b/test-ffi/scripts/process_json.js @@ -27,64 +27,72 @@ const attributeIndex = args[4] || 0; // Read the file at the specified path. const rawData = fs.readFileSync(path, "utf8"); +let formattedJson; + try { // Parse the raw data as JSON. - const formattedJson = JSON.parse(rawData); - - // Example JSON: - // { - // "name": "Example NFT #0", - // "description": "This is an example NFT", - // "image": "data:image/svg+xml;0", - // "attributes": [ - // { - // "trait_type": "Example Attribute", - // "value": "Example Value" - // }, - // { - // "trait_type": "Number", - // "value": "0", - // "display_type": "number" - // }, - // { - // "trait_type": "Parity", - // "value": "Even" - // } - // ] - // } - - if (responseType === '--top-level') { - // Extract the name, description, and image from the JSON. - const itemName = formattedJson.name; - const description = formattedJson.description; - const image = formattedJson.image; - - // Initialize the typeArray and valueArray with the name, description, and - // image. - let typeArray = ['string', 'string', 'string']; - let valueArray = [itemName, description, image]; - - const abiEncoded = ethers.utils.defaultAbiCoder.encode(typeArray, valueArray); - - // Write the abiEncoded data to stdout. - process.stdout.write(abiEncoded); - } else if (responseType === '--attribute') { - // Extract the attributes from the JSON. - const attributes = formattedJson.attributes; - - const traitType = attributes[attributeIndex].trait_type; - const traitValue = attributes[attributeIndex].value; - const traitDisplayType = attributes[attributeIndex].display_type || "noDisplayType"; - - // Encode the typeArray and valueArray. - const abiEncoded = ethers.utils.defaultAbiCoder.encode( - ['string', 'string', 'string'], [traitType, traitValue, traitDisplayType] - ); - - // Write the abiEncoded data to stdout. - process.stdout.write(abiEncoded); - } + formattedJson = JSON.parse(rawData); } catch (e) { // JSON.parse failed. Likely the path to the file is wrong, the json is not populated, or the JSON is malformed. process.stdout.write('0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000004a534f4e2e7061727365206661696c65642e204c696b656c7920746865207061746820746f207468652066696c652069732077726f6e672c20746865206a736f6e206973206e6f7420706f70756c617465642c206f7220746865204a534f4e206973206d616c666f726d65642e00000000000000000000000000000000000000'); -} \ No newline at end of file +} + +// Example JSON: +// { +// "name": "Example NFT #0", +// "description": "This is an example NFT", +// "image": "data:image/svg+xml;0", +// "attributes": [ +// { +// "trait_type": "Example Attribute", +// "value": "Example Value" +// }, +// { +// "trait_type": "Number", +// "value": "0", +// "display_type": "number" +// }, +// { +// "trait_type": "Parity", +// "value": "Even" +// } +// ] +// } + +if (responseType === '--top-level') { + // Extract the name, description, and image from the JSON. + const itemName = formattedJson.name; + const description = formattedJson.description; + const image = formattedJson.image; + + // Initialize the typeArray and valueArray with the name, description, and + // image. + let typeArray = ['string', 'string', 'string']; + let valueArray = [itemName, description, image]; + + const abiEncoded = ethers.utils.defaultAbiCoder.encode(typeArray, valueArray); + + // Write the abiEncoded data to stdout. + process.stdout.write(abiEncoded); +} else if (responseType === '--attribute') { + // Extract the attributes from the JSON. + const attributes = formattedJson.attributes; + + try { + + const traitType = attributes[attributeIndex].trait_type; + const traitValue = attributes[attributeIndex].value; + const traitDisplayType = attributes[attributeIndex].display_type || "noDisplayType"; + + // Encode the typeArray and valueArray. + const abiEncoded = ethers.utils.defaultAbiCoder.encode( + ['string', 'string', 'string'], [traitType, traitValue, traitDisplayType] + ); + + // Write the abiEncoded data to stdout. + process.stdout.write(abiEncoded); + } catch (e) { + // Likely the attributeIndex is out of bounds. + process.stdout.write('0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002b4c696b656c792074686520617474726962757465496e646578206973206f7574206f6620626f756e64732e000000000000000000000000000000000000000000'); + } +} From f035143b77f7c695105e232b08e3f1d7efa0f69e Mon Sep 17 00:00:00 2001 From: djviau Date: Thu, 28 Sep 2023 10:02:36 -0400 Subject: [PATCH 2/2] fix gh workflow --- .github/workflows/test-ffi.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-ffi.yml b/.github/workflows/test-ffi.yml index d882080..41f0757 100644 --- a/.github/workflows/test-ffi.yml +++ b/.github/workflows/test-ffi.yml @@ -54,6 +54,6 @@ jobs: - name: Clear Temp Files run: | cd ./test-ffi/tmp - rm temp.json + find . -maxdepth 1 -name 'temp*' -exec rm -f {} \; cd ../.. id: clear