Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix ffi tests #11

Merged
merged 2 commits into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
}
}
Loading