Skip to content

Commit

Permalink
Merge pull request #11 from ProjectOpenSea/dan/2023/09/fix-ffi-tests
Browse files Browse the repository at this point in the history
fix ffi tests
  • Loading branch information
DJViau authored Sep 28, 2023
2 parents 5bc2d67 + f035143 commit 31b49fd
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 99 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test-ffi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
41 changes: 18 additions & 23 deletions test-ffi/onchain/svg.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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));
}
}
48 changes: 30 additions & 18 deletions test-ffi/reference/ExampleNFT.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;<svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"500\\" height=\\"500\\" ><rect width=\\"500\\" height=\\"500\\" fill=\\"lightgray\\" /><text x=\\"50%\\" y=\\"50%\\" dominant-baseline=\\"middle\\" text-anchor=\\"middle\\" font-size=\\"48\\" fill=\\"black\\" >',
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"500\" height=\"500\" ><rect width=\"500\" height=\"500\" fill=\"lightgray\" /><text x=\"50%\" y=\"50%\" dominant-baseline=\"middle\" text-anchor=\"middle\" font-size=\"48\" fill=\"black\" >",
vm.toString(tokenId),
"</text></svg>"
)
);
// abi.encodePacked(
// 'data:image/svg+xml;<svg xmlns=\\"http://www.w3.org/2000/svg\\" width=\\"500\\" height=\\"500\\" ><rect width=\\"500\\" height=\\"500\\" fill=\\"lightgray\\" /><text x=\\"50%\\" y=\\"50%\\" dominant-baseline=\\"middle\\" text-anchor=\\"middle\\" font-size=\\"48\\" fill=\\"black\\" >',
// vm.toString(tokenId),
// "</text></svg>"
// )
}

function _generateError(uint256 tokenId, uint256 traitIndex, string memory message)
Expand All @@ -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));
}
}
120 changes: 64 additions & 56 deletions test-ffi/scripts/process_json.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" width=\\\"500\\\" height=\\\"500\\\" ><rect width=\\\"500\\\" height=\\\"500\\\" fill=\\\"lightgray\\\" /><text x=\\\"50%\\\" y=\\\"50%\\\" dominant-baseline=\\\"middle\\\" text-anchor=\\\"middle\\\" font-size=\\\"48\\\" fill=\\\"black\\\" >0</text></svg>",
// "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');
}
}

// Example JSON:
// {
// "name": "Example NFT #0",
// "description": "This is an example NFT",
// "image": "data:image/svg+xml;<svg xmlns=\\\"http://www.w3.org/2000/svg\\\" width=\\\"500\\\" height=\\\"500\\\" ><rect width=\\\"500\\\" height=\\\"500\\\" fill=\\\"lightgray\\\" /><text x=\\\"50%\\\" y=\\\"50%\\\" dominant-baseline=\\\"middle\\\" text-anchor=\\\"middle\\\" font-size=\\\"48\\\" fill=\\\"black\\\" >0</text></svg>",
// "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');
}
}

0 comments on commit 31b49fd

Please sign in to comment.