Skip to content

Instantly share code, notes, and snippets.

@lobarrel
Last active July 6, 2023 19:57
Show Gist options
  • Save lobarrel/a8dc88112038c1eb77099a6116b75d47 to your computer and use it in GitHub Desktop.
Save lobarrel/a8dc88112038c1eb77099a6116b75d47 to your computer and use it in GitHub Desktop.
message-generator-code-description

Message Generator code description

The purpose of this document is to provide a detailed explanation of the code related to the message-generator and it serves as a tool for developers to quickly understand the code structure and the main steps the message-generator does for test execution. The following code analysis is divided into sections that represent the different source files of the message-generator, making it easier to find references in the code.

main.rs

  1. The code defines two enums: Sv2Type, which contains all data types used in SV2, and ActionResult, which contains the possible values representing the results of an action defined inside the test file.
  2. The code defines the structs Upstream and Downstream, which represent the connection settings of the tested SV2 role.
  3. The code defines a struct Action, which represents an action that has to be execute and the result of which has to be checked as defined inside the test file.
  4. The code defines a struct Command that represents a shell command to be executed.
  5. The code defines a struct Test that represents the parsed contents from a test configuration file.
  6. The code defines the main function, which is the entry point of the program.
  7. The main function reads command-line arguments, loads a test file, parses it, and creates an executor.
  8. The executor executes the shell commands and actions defined in the test file.

Overall, the code defines a test framework for executing tests defined in a test configuration file. It performs actions and executes shell commands based on the parsed test file.

executor.rs

The Executor struct is responsible for executing a series of actions based on a provided test configuration.

Here's a summary of what the code does:

  1. It defines a struct called Executor that contains several fields related to the information contained inside the test file. The Executor struct has an associated implementation block (impl) that defines its methods.
  2. The new method of the Executor struct is an asynchronous function that takes a Test object and a test_name string as parameters.
    • It creates a new instance of the Executor struct.
    • A loop iterates over the setup commands of the test. It handles different types of commands such as "kill" and "sleep", as well as executing OS commands using the os_command function.
    • After the setup commands are executed, the method checks the test's as_downstream and as_upstream properties to determine how to set up the communication channels. Depending on the presence or absence of as_downstream and as_upstream, the method sets up the communication channels using the setup_as_downstream and setup_as_upstream functions.
  3. The execute method of the Executor struct is an asynchronous function that executes the actions defined in the test configuration.
    • A loop that iterates over the actions defined in the test. For each action, the method checks the role (Upstream or Downstream) and retrieves the corresponding sender and receiver channels.
    • The method sends messages from the action's messages field to the appropriate channel and receives a response.
    • The method then checks the received response against the expected results defined in the action's result field.
    • If the received response does not match the expected results, the method sets the success flag to false.
    • Finally, the method prints various debug information about the executed actions and results.

Overall, the code implements an Executor struct that orchestrates the execution of actions based on a provided test configuration, communicating with different roles (Upstream and Downstream) and checking the received responses against expected results.

net.rs

Two asynchronous functions are defined: setup_as_upstream and setup_as_downstream, which facilitate the setup of network connections.

  1. The setup_as_upstream function takes the following parameters:
  • socket: The socket address to bind the TCP listener.
  • keys: An optional tuple containing an encoded Ed25519 public key and an encoded Ed25519 secret key.
  • execution_commands: A vector of Command structs representing execution commands.
  • childs: A mutable vector of Option<tokio::process::Child>.

The function begins by binding a TCP listener to the provided socket address using TcpListener::bind. It then iterates over each Command in the execution_commands vector and spawns an OS command using the os_command function. The resulting child processes are stored in the childs vector.

Next, the function accepts an incoming TCP connection using listner.accept().await. It retrieves the socket stream and ignores the corresponding address.

Based on the presence of keys, the function follows two different paths:

  • If keys is Some, it extracts the encoded Ed25519 public key and secret key from the tuple. It creates a Responder using Responder::from_authority_kp with the public and secret keys, along with a duration. It then creates a Connection with the stream and HandshakeRole::Responder using the Connection::new function.
  • If keys is None, it creates a PlainConnection with the stream using the PlainConnection::new function.

Finally, the function returns a tuple containing the receiver and sender of EitherFrame<Message>.

  1. The setup_as_downstream function is similar to setup_as_upstream, but with a few differences:
  • It takes socket as the socket address to connect to.
  • It takes key as an optional encoded Ed25519 public key.
  • It directly connects to the specified socket address using TcpStream::connect.
  • Based on the presence of key, it either creates an Initiator or a PlainConnection using the Connection::new or PlainConnection::new functions, respectively.

The function then returns a tuple containing the receiver and sender of EitherFrame<Message>.

In summary, these functions provide convenient ways to set up network connections, either as an upstream or a downstream role, with or without encryption using Ed25519 keys, and with optional execution of additional commands.

/parser

mod.rs

Here the code defines a parser for the test configuration. The test configuration is provided as a JSON string and is parsed into a structured representation. The parsing is done in several steps, each represented by an enum variant of the Parser type.

Here is a summary of each step:

  1. Step 1: In this step, the test messages in the configuration are parsed and stored in a HashMap with message IDs as keys and AnyMessage objects as values. The AnyMessage type is a generic representation of a message that can be parsed later.

  2. Step 2: This step takes the parsed messages from Step1, creates Sv2Frames (frames of SV2 protocol) identified by message IDs and stores them in a HashMap.

  3. Step 3: In this step the actions are parsed into a vector of Action.

  4. Step 4: In this step, the setup, execution and cleanup commands, as well as roles, are parsed. The setup, execution, and cleanup commands are extracted from the test configuration and stored as Command objects in separate vectors. The roles (client, server, proxy) are parsed into Upstream or Downstream.

Then the parsed data is prepared for execution. A new Test object, containing all the necessary information for executing the test, is created. This includes the actions, role-specific information (such as upstream and downstream details), and the setup, execution, and cleanup commands.

sv2_messages.rs

Here we find the definition of several data structures and functions related to SV2 message parsing and serialization.

  1. The TestMessageParser structure represents a parsed message. It contains optional fields for different types of messages: common messages, job negotiation messages, mining messages, and template distribution messages.

  2. The into_map method of TestMessageParser converts the parsed messages into a HashMap, where the ID is the key and the corresponding message is the value. It iterates over each type of message, extracts the ID and the message, and inserts them into the HashMap.

  3. The from_str method of TestMessageParser parses a JSON string into a TestMessageParser structure using the serde_json crate.

  4. There are several enum definitions (CommonMessages, TemplateDistribution, JobNegotiation, and Mining) that represent different types of messages with associated data. These enums implement conversion methods to convert the defined message types into the corresponding types from the roles_logic_sv2::parsers module.

In summary, this code provides functionality for parsing and serializing different types of messages and allows retrieving specific messages by ID from a parsed message structure.

frames.rs

A struct called Frames is defined. It contains a HashMap called frames, which maps strings to Sv2Frame<AnyMessage<'a>, Slice> objects.

The method from_step_1 creates an instance of Frames from input data. It takes two parameters: a string test and a HashMap called messages.

  1. It parses the test string as JSON into a Map<String, Value> using the serde_json::from_str function.

  2. It retrieves the value associated with the key "frame_builders" from the JSON map and assumes it is an array. It then iterates over each element in the array.

  3. For each frame, it extracts the message ID as a string and splits it using "::" as the delimiter.

    • If the resulting id vector has a length of 1, it means the message is contained in the present file. It looks up the message in the messages hashmap using the ID and clones it. The id is stored in a variable for later use.

    • If the id vector has a length of 2, it means the message appears in a different file. It constructs the path by prepending "../../../../" to the first element of the id vector. Then it calls the message_from_path function with the path and the second element of the id vector, and assigns the result to the message variable. The id is also stored for later use.

    • If the id vector has a length other than 1 or 2, it raises an error.

  4. It then retrieves the value associated with the key "type" from the frame and assumes it is a string. Based on the value of type_, it performs different actions.

    • If type_ is "automatic", it attempts to convert the message into an Sv2Frame<AnyMessage<'a>, Slice> using the try_into method, and inserts it into the result hashmap using the id as the key.

    • If type_ is "manual", it extracts the message type, extension type, and channel_msg values from the frame. It converts the message type from a hexadecimal string to a u8 value, removes underscores from the extension type string and parses it as a u16 value, and retrieves the channel_msg as a boolean value. It then creates an Sv2Frame using the Sv2Frame::from_message method, passing in the message, message type, extension type, and channel_msg. It inserts the resulting frame into the result hashmap using the id as the key.

    • If type_ has any other value, it raises an error.

Finally, it creates an instance of Frames with the result hashmap and returns it.

In summary, this code parses JSON input, retrieves frame data, and constructs Sv2Frame objects based on the frame type and associated messages.

actions.rs

A struct called ActionParser is defined. It doesn't have any fields or state as it serves as a container for the from_step_3 method.

The method from_step_3 parses JSON input and constructs a vector of Action objects. It takes two parameters: a string test and a HashMap called frames.

  1. It parses the test string as JSON into a Map<String, Value> using the serde_json::from_str function.

  2. It retrieves the value associated with the key "actions" from the JSON map and assumes it is an array. It then iterates over each element in the array.

  3. For each action, it retrieves the value associated with the key "role" from the action and assumes it is a string. Based on the value of role, it assigns the corresponding Role enum variant (Role::Downstream or Role::Upstream) to the role variable. If the value is neither "client" nor "server", it raises an error.

  4. It initializes an empty vector called action_frames and retrieves the value associated with the key "message_ids" from the action. It assumes it is an array and iterates over each element in the array. For each message ID, it looks up the corresponding frame in the frames hashmap using the ID as the key. If the frame is found, it clones it and wraps it in a StandardEitherFrame::Sv2. The resulting frame is then added to the action_frames vector.

  5. It retrieves the value associated with the key "actiondoc" from the action. If the value is Some, it converts it to a string and assigns it to the actiondoc variable. If the value is None, it assigns None to the actiondoc variable.

  6. It initializes an empty vector called action_results and retrieves the value associated with the key "results" from the action. It assumes it is an array and iterates over each element in the array. For each result, it retrieves the value associated with the key "type" and assumes it is a string. Based on the value of type, it performs different actions:

    • If type is "match_message_type", it retrieves the value associated with the key "value" from the result. It assumes it is a hexadecimal string and converts it to a u8 value. It constructs an ActionResult::MatchMessageType variant with the message type and adds it to the action_results vector.

    • If type is "match_message_field", it retrieves the value associated with the key "value" from the result. It assumes it is a JSON value and attempts to deserialize it into a tuple (String, String, Vec<(String, Sv2Type)>). If the deserialization is successful, it constructs an ActionResult::MatchMessageField variant with the deserialized value and adds it to the action_results vector.

    • If type is "match_message_len", it retrieves the value associated with the key "value" from the result. It assumes it is an unsigned 64-bit integer and converts it to a usize value. It constructs an ActionResult::MatchMessageLen variant with the message length and adds it to the action_results vector.

    • If type is "match_extension_type", it retrieves the value associated with the key "extension_type" from the result. It assumes it is a string, removes underscores from it, and parses it as a u16 value. It constructs an ActionResult::MatchExtensionType variant with the extension type and adds it to the action_results vector.

    • If type is "close_connection", it adds an ActionResult::CloseConnection variant to the action_results vector.

    • If type is "none", it adds an ActionResult::None variant to the action_results vector.

    • If type has any other value, it raises an error.

Finally, it constructs an Action object using the action_frames, action_results, role, and actiondoc variables, and adds it to the result vector. After iterating over all actions, it returns the result vector containing all constructed Action objects.

In summary, this code parses JSON input, retrieves action data, and constructs Action objects with associated messages, results, roles, and action documents.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment