Ultratest Usage
This document pertains to developers who want to write smart contracts.
Obtaining ultratest
We have created a Docker image that has pre-created scripts, tools, and pre-packaged binaries. ultratest
is already included inside of the Docker image.
Individual binaries are not currently available for download.
Usage
Inside the Docker Container the following can be executed for general usage.
ultratest --help
Nodeos instances
Nodeos instances are automatically created in the background with a preset chain configuration.
The first instance will have the following deployed:
- bios contract
- protocol configurations
- system contract
- token contract
- msig contract
- various initialization actions for ram market, system currency, etc.
After the first instance has this data deployed it will take a snapshot.
This snapshot will be used to quickly reboot the chain and run each test file as necessary.
Additional instances will be launched if additional producers are needed in a test file.
Keosd Instances
Keosd is launched and a single wallet is created which will contain all keys that were used during the boot process for the framework.
All keys can be seen by running either of these two commands inside of the docker image.
cleos wallet keys
cleos wallet private_keys
The wallet password can be found at ~/ultratest/wallet.txt
.
Data Persistence
During each test a system snapshot is used between each test file.
All data is wiped if a new instance of ultratest
is ran.
However, a configuration inside of test files exists to prevent any additional rollbacks during chained tests. See Understanding the Test Class
Warning on Naming Files
The way that ultratest
kills nodeos
processes is very indiscriminate.
It matches nodeos
and keosd
in pkill as a search string, meaning if you are running other processes with nodeos
or keosd
, it will kill them as well.
This affects tests when running singular tests, for instance ultratest -t tests/nodeos_test.ultra_test.js
will kill the test suite execution because the process has nodeos
within the cmd.
Starting a System Node
Options exist to start a node with system contracts deployed and will function as a normal node until you kill the nodeos instance.
ultratest -D -n --system
Writing Tests
Test files must have a suffix of ultra_test.js
in order to be recognized by the ultratest
framework.
Incorrect -> hello-world-test.js
Correct -> hello-world.ultra_test.js
Basic Format
Our test format is very specific but also has plenty of options to get most smart contract developers off the ground.
module.exports = class test {
constructor() {}
// Deploys ultra system contracts to the nodeos instance
requiresSystemContracts() {
return true;
}
// What account to create, and what contract to deploy on it
importContracts() {
return [{ account: 'smrtcntract1', path: '../contracts', contract: 'hello' }];
}
// Created after importing contracts
requiredAccounts() {
return ['account1', 'account2', 'account3', 'account4'];
}
tests({ assert, endpoint, cleos, rpc, api, ecc, keychain }) {
assert(true, "This will never trigger because it is true.");
return {
'should execute transaction': async () => {
// Something should happen in this test.
};
}
};
Understanding the Test Class
The various class functions inside of the file above all have their own use cases.
- requiresSystemContracts()
- Deploys system contracts to the nodeos instance for this test.
- Must return a
boolean
.
- importContracts()
- Imports smart contracts based on account, path of the contract, and the contract name.
- Must return an Array of Objects: *
[{ account: string, path: string, contract: string }]
- requiredAccounts()
- A string list of accounts to create before running the tests.
- Must return an Array of Strings
- requiredUnlimitedAccounts()
- A string list of unlimited accounts to create before running tests.
- These accounts have zero restrictions on resources.
- Must return an Array of Strings
- nodeosConfigs()
- A way to change
genesis.json
andconfig.ini
options for nodeos instance. - Must return an Object of Objects. * { genesis: {}, config: {} }
- A way to change
- requiredProducers()
- Creates multiple producers for the chain that produce blocks.
- Must return an Array of Objects:
[{ name:'', config:{}, node:0'}]
- name is the producer name
- node is the nodeos instance
- Optional - config is a custom
config.ini
for this producer
- requiredUnlimitedSupply()
- Specifies whether system will use max supply when creating the system token.
- Must return boolean
- preventRollback()
- Specifies whether the chain should rollback before this test is started.
- Must return boolean
tests() class function
Tests usually passes a handful of useful objects from commonly used libraries like eosjs, eos-ecc, etc.
tests({ assert, endpoint, cleos, rpc, api, ecc, keychain }) {
assert(true, "This will never trigger because it is true.");
return {
'should execute transaction': async () => {
// Something should happen in this test.
};
}
assert(condition, error_message)
- Standard issue testing suite asserter.
- Standard issue testing suite asserter.
endpoint
- The URL for the running nodeos (port might not always be 8888)
- The URL for the running nodeos (port might not always be 8888)
cleos(command, options)
- A helper method that will execute all cleos commands fed to it.
- options is an object that takes the following properties:
{ swallow:boolean, // whether to swallow errors from this cleos command fetch:boolean // if true cleos will return the results, if false cleos will return a boolean based on success. }
rpc
a standardeosjs
rpc object (Getting table information using eosjs)api
a standardeosjs
api object (Sending a transaction using eosjs)ecc
a standardeosjs-ecc
object (Full documentation here)common
a group of functions that are most commonly used for fetchingeosio
chain data, creating accounts, sending tokens, etc.- See below.
keychain
- See below.
keychain object
Ultratest follows a deterministic keys schema, where the keys for each account/permission will be exactly the same each run. For example the key for foo
will always be the same, but bar
will be different from foo
.
generateAndReturnPublicKey(account:string): Promise<string>
* This will return a public key which is created for this account, it will also inject that key into both eosjs and keosd for signing. You can specifyaccount@permission
in theaccount
parameter.getAccountKeys(account:string): Array<string>|null
- will return all available keys or null.
- will return all available keys or null.
setAccountKeys(account:string, key_object:EosjsKey): Promise<void>
- will allow setting a private key for an account, and will also import it into eosjs and keosd. Will not override!. Must be a key object (eosjs).
- will allow setting a private key for an account, and will also import it into eosjs and keosd. Will not override!. Must be a key object (eosjs).
getAllPublicKeys(): Array<string>
- Will get all available public keys.
- Will get all available public keys.
getPrivateKey(public_key:string): string|null
- Will return any private key found matching this public key.
- Will return any private key found matching this public key.
getPublicKeyFromAccount(account:string): string|null
- Will return a public key associated with an account.
- Will return a public key associated with an account.
getPrivateKeyFromAccount(account:string): string|null
- Will return a public key associated with an account.
- Will return a public key associated with an account.
sign(signargs:object): Promise<{ signatures, serializedTransaction }>
- Will sign a transaction following the
eosjs
signature provider format. Keys will be inferred from the authorizations & required keys.
- Will sign a transaction following the
Writing a Simple Test
Below is an example of writing a simple test that checks the value of a global eosio
table to ensure that it is a specific value. It also checks if the eosio
account has been created.
module.exports = class test {
constructor() {}
requiredAccounts() {
return ['annie'];
}
requiresSystemContracts() {
return true;
}
tests({ assert, endpoint, cleos, rpc, api, ecc, keychain, common }) {
return {
'should perform an account lookup against eosio': async () => {
const account = await common.getAccount('eosio');
assert(account, "Account 'eosio' does not exist.");
},
'should perform an account lookup against annie': async () => {
const account = await common.getAccount('eosio');
assert(account, "Account 'eosio' does not exist.");
},
'should lookup eosio global table': async () => {
const results = await common.getTable('eosio', 'eosio', 'global');
assert(results[0] && result[0].ultra_veto_enabled, 'Table was found, and vote is enabled.');
},
};
}
};
assert
can be used in a handful of ways, but the most common way to write an assert
is to check if a value is true or false.
Common API
The common
object inside of tests
has a handful of useful functions that will speed up your test deployment time.
getSingleton
Returns a single table object with what is presumed to be the table data inside of it.
Usage
getSingleton(account: string, scope: string, table: string);
Example
const result = common.getSingleton('eosio', 'eosio', 'global');
if (!result) {
// No table exists or the function already errored out.
return;
}
assert(result.ultra_veto_enabled === 1, 'veto was not enabled');
getTable
Returns entries from the table.
Usage
getTable(account: string, scope: string, table: string, limit = 100, showMore = true);
limit - The number of entries to show.
showMore - This will return the entire table object with a showMore
boolean that will be toggled to true if there are more entries.
Example
const results = await common.getTable('eosio', 'eosio', 'producers');
assert(Array.isArray(results.rows), 'More than two rows are present');
Example Data Output
{
"rows": [
{
"code": 'eosio',
"scope": 'eosio',
"table": 'producers',
"payer": 'eosio',
"count": 1
},
],
"more": ''
}
getScopes
Returns an array of available tables, and scopes for a given contract.
Usage
getTable(account: string);
Example
const scopes = await common.getScopes('eosio.token');
assert(scopes.rows.length >= 1, 'Did not retrieve any scopes from eosio.token');
Example Data Output
{
"rows": [
{
"code": 'eosio.token',
"scope": '........ehbp5',
"table": 'stat',
"payer": 'eosio.token',
"count": 1
},
],
"more": ''
}
getAccount
Returns the account information if the account is available. Otherwise returns null.
Usage
getAccount(account: string);
Example
const account = await common.getAccount('bobbyjoe');
assert(account, 'Did not find account bobbyjoe');
Example Data Output
{
account_name: 'bobbyjoe',
head_block_num: 47,
head_block_time: '2022-05-02T17:59:26.500',
privileged: false,
last_code_update: '1970-01-01T00:00:00.000',
created: '2022-05-02T17:59:26.000',
core_liquid_balance: '100.00000000 UOS',
ram_quota: 256000,
net_weight: 0,
cpu_weight: 0,
net_limit: { used: -1, available: -1, max: -1 },
cpu_limit: { used: -1, available: -1, max: -1 },
ram_usage: 380,
permissions: [
{ perm_name: 'active', parent: 'owner', required_auth: [Object] },
{ perm_name: 'owner', parent: '', required_auth: [Object] }
],
total_resources: {
owner: 'bobbyjoe',
power_weight: '0.00000000 UOS',
ram_bytes: 256000,
flags: 0
},
self_delegated_bandwidth: null,
refund_request: null
}
createAccount
Creates an account and returns true
if account creation was successful.
Usage
createAccount(account: string, ram = 250, tokens = 100);
Example
const result = await common.createAccount('bobbyjoe');
assert(result, 'account bobbyjoe was not created');
addUOS
Adds tokens to a specific account.
Returns the transaction result if successful.
Usage
addUOS(name: string, amount: string | number);
amount - must be a whole number
Example
const result = await common.addUOS('bobbyjoe', 100);
transfer
Transfers tokens from an account to another account.
Returns the transaction result if successful.
Usage
transfer(from: string, to: string, fixedAmount: number, memo = '');
Example
const result = await common.transfer('bobbyjoe', 'alice', 5.00004321, 'moneys!');
transactAssert
Used to verify that a certain smart contract will assert a certain message when it fails. Otherwise returns false
.
If successful it will return true if transaction succeeds.
Usage
transactAssert(
actions: Array<{ account: string, name: string, authorization: Array<{ actor: string, permissions: string}>, data: Object}>,
assertionMessage: string
);
Example
const memo = [...Array(257).keys()].map(x => 'a').join('');
const res = await common.transactAssert(
[
{
account: 'eosio.nft.ft',
name: 'limitmint',
authorization: [{ actor: 'somemanager', permission: 'active' }],
data: { token_factory_id: 1, account_minting_limit: 10, memo },
},
],
'memo has more than 256 bytes',
);
pushAction
Pushes a normal cleos
based transaction.
Returns transaction if successful.
Usage
pushAction(code: string, action: string: authority: string, args: Array<any>)
Example
const result = await common.pushAction(
"eosio.token",
"transfer",
"ultra.eosio@active",
["ultra.eosio", "alice", "4.00000000 UOS", ""]
);
assert(result, "Did not push transaction with 'pushAction' common api");
getBalance
Returns the current numerical
representation of the user's system balance.
Usage
getBalance(account: string)
Example
const currentBalance = await common.getBalance('alice');
assert(currentBalance >= 1, "Did not have any tokens");
addCodePermission
Adds a code permission to a smart contract account permission.
Returns successful transaction or throws error.
Usage
addCodePermission(account: string, permission = 'active');
Example
const result = await common.addCodePermission("alice", "active");
sleep
Milliseconds to wait before moving to next part of a function.
Usage
sleep(milliseconds: number);
Example
await common.sleep(500);
post
Performs a simple post request against any available test API.
Returns the fetched data in object format.
Usage
post(endpoint: string, body: Object);
Example
const result = await common.post("chain/get_account", {
account_name: "alice",
expected_core_symbol: "8,UOS",
});
assert(result, "Could not perform a POST request");
get
Performs a simple get request against any available test API.
Returns the fetched data in object format.
Usage
get(endpoint: string);
Example
const result = await common.post("chain/get_info");
console.log(result);
updateAuth
Updates the permissions for an account.
Usage
updateAuth(account: string, parent: string, permission: string, weight: number, keys: Array<Object>, accounts: Array<Object>);
Example
const result = await common.updateAuth(
"alice",
"active",
"newperm",
1,
[],
[
{
weight: 1,
permission: {
actor: "bobbyjoe",
permission: "active",
},
},
]
);