├── .ci └── check │ └── check ├── .github ├── actions │ ├── check │ ├── deploy-dep-package │ │ ├── action.yml │ │ └── exec.sh │ ├── docker-build-and-push │ │ └── action.yml │ ├── docker-run │ │ ├── action.yml │ │ └── exec.sh │ ├── licenses │ └── minio-client │ │ ├── action.yml │ │ └── exec.sh └── workflows │ └── check.yml ├── .gitignore ├── .gitlab-ci.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── murf-macros ├── Cargo.toml ├── LICENSE ├── README.md └── src │ ├── expect_call.rs │ ├── lib.rs │ ├── misc │ ├── attribs_ex.rs │ ├── formatted_string.rs │ ├── generics_ex.rs │ ├── item_impl_ex.rs │ ├── iter_ex.rs │ ├── method_ex.rs │ ├── mod.rs │ ├── return_type_ex.rs │ ├── temp_lifetimes.rs │ └── type_ex.rs │ └── mock │ ├── context.rs │ ├── expectation.rs │ ├── expectation_builder.rs │ ├── expectation_module.rs │ ├── handle.rs │ ├── mock.rs │ ├── mock_method.rs │ ├── mock_module.rs │ ├── mockable.rs │ ├── mockable_default.rs │ ├── mocked.rs │ ├── mod.rs │ ├── parsed.rs │ └── shared.rs ├── murf ├── Cargo.toml ├── LICENSE ├── README.md ├── src │ ├── action │ │ ├── invoke.rs │ │ ├── mod.rs │ │ └── returns.rs │ ├── example.rs │ ├── lib.rs │ ├── local_context.rs │ ├── matcher │ │ ├── any.rs │ │ ├── closure.rs │ │ ├── compare.rs │ │ ├── deref.rs │ │ ├── inspect.rs │ │ ├── mod.rs │ │ ├── multi.rs │ │ ├── no_args.rs │ │ ├── range.rs │ │ └── string.rs │ ├── misc.rs │ ├── sequence.rs │ ├── times.rs │ └── types │ │ ├── duration.rs │ │ └── mod.rs └── tests │ ├── actions │ ├── invoke.rs │ ├── mod.rs │ ├── return_once.rs │ └── return_pointee.rs │ ├── interface │ ├── argument_with_default_lifetime.rs │ ├── argument_with_lifetime.rs │ ├── associated_functions.rs │ ├── associated_type_trait.rs │ ├── associated_type_trait_with_lifetime.rs │ ├── clonable_mock.rs │ ├── constructor_with_args.rs │ ├── expect_call.rs │ ├── expect_call_with_const_generics.rs │ ├── expect_call_with_generics.rs │ ├── exsiting_type.rs │ ├── generic_associated_type_trait.rs │ ├── generic_trait.rs │ ├── in_sequence.rs │ ├── local_context.rs │ ├── mock_lifetime.rs │ ├── mod.rs │ ├── no_default.rs │ ├── reference_argument.rs │ ├── return_self_type.rs │ ├── self_arc.rs │ ├── sequence.rs │ ├── simple_trait.rs │ ├── times.rs │ └── trait_bound_with_self_type.rs │ ├── matcher │ ├── deref.rs │ ├── mod.rs │ ├── multi_args.rs │ └── range.rs │ └── mod.rs └── rust-toolchain.toml /.ci/check/check: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | export PACKAGES=" \ 6 | murf,true,true,true,70.0 \ 7 | murf-macros,true,true,true \ 8 | " 9 | 10 | export SCRIPT_DIR="$(dirname $0)" 11 | export PROJECT_ROOT="$(readlink -f "$SCRIPT_DIR/../..")" 12 | export COVERAGE_DIR="$PROJECT_ROOT/target/coverage" 13 | 14 | "$PROJECT_ROOT/.github/actions/check" 15 | -------------------------------------------------------------------------------- /.github/actions/check: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # Whether to check the code format or not 6 | RUN_CARGO_FMT=${RUN_CARGO_FMT:-true} 7 | 8 | # Globally enable or disable cargo clippy execution 9 | RUN_CARGO_CLIPPY=${RUN_CARGO_CLIPPY:-true} 10 | 11 | # Globally enable or disable cargo doc execution 12 | RUN_CARGO_DOC=${RUN_CARGO_DOC:-true} 13 | 14 | # Globally enable or disable cargo test execution 15 | RUN_CARGO_TEST=${RUN_CARGO_TEST:-true} 16 | 17 | # Globaly enable or disable cargo test execution with code coverage 18 | RUN_CARGO_TEST_WITH_COVERAGE=${RUN_CARGO_TEST_WITH_COVERAGE:-true} 19 | 20 | # Globale enable or disable code coverage report generation 21 | GENERATE_COVERAGE_REPORT=${GENERATE_COVERAGE_REPORT:-false} 22 | 23 | 24 | PROJECT_ROOT="${PROJECT_ROOT:?"Environment variable is unset!"}" 25 | COVERAGE_DIR="${COVERAGE_DIR:?"Environment variable is unset!"}" 26 | 27 | OLD_PWD="$PWD" 28 | 29 | finish() { 30 | cd "$OLD_PWD" 31 | } 32 | 33 | trap finish EXIT 34 | 35 | cd "$PROJECT_ROOT" 36 | 37 | printf "\n\n--== Build Environment ==--\n\n" 38 | rustc \ 39 | --version 40 | cargo \ 41 | --version 42 | 43 | parsePackgeTuple() { 44 | OLDIFS=$IFS; 45 | IFS=','; 46 | set -- $1; 47 | IFS=$OLDIFS 48 | 49 | PACKAGE="$1" 50 | DO_RUN_CARGO_CLIPPY="$2" 51 | DO_RUN_CARGO_DOC="$3" 52 | DO_RUN_CARGO_TEST="$4" 53 | EXPECTED_COVERAGE="$5" 54 | } 55 | 56 | # Run cargo fmt 57 | if [ "$RUN_CARGO_FMT" = "true" ]; then 58 | printf "\n\n--== Check Code Style ==--\n\n" 59 | cargo fmt \ 60 | --all \ 61 | -- \ 62 | --check 63 | fi 64 | 65 | # Run cargo clippy 66 | if [ "$RUN_CARGO_CLIPPY" = "true" ]; then 67 | for ARGS in $PACKAGES; do 68 | parsePackgeTuple "$ARGS" 69 | 70 | if [ "$DO_RUN_CARGO_CLIPPY" = "false" ]; then 71 | continue 72 | fi 73 | 74 | cd "$PROJECT_ROOT/$PACKAGE" 75 | 76 | printf "\n\n--== Run Linter for $PACKAGE ==--\n\n" 77 | 78 | cargo clippy \ 79 | --all-targets \ 80 | $CARGO_EXTRA_ARGS \ 81 | -- \ 82 | --deny "warnings" 83 | done 84 | fi 85 | 86 | # Run cargo doc 87 | if [ "$RUN_CARGO_DOC" = "true" ]; then 88 | for ARGS in $PACKAGES; do 89 | parsePackgeTuple "$ARGS" 90 | 91 | if [ "$DO_RUN_CARGO_DOC" = "false" ]; then 92 | continue 93 | fi 94 | 95 | cd "$PROJECT_ROOT/$PACKAGE" 96 | 97 | printf "\n\n--== Check Doc for $PACKAGE ==--\n\n" 98 | 99 | RUSTDOCFLAGS="-D warnings" \ 100 | cargo doc \ 101 | --no-deps \ 102 | $CARGO_EXTRA_ARGS \ 103 | --document-private-items 104 | done 105 | fi 106 | 107 | # Run cargo test 108 | for ARGS in $PACKAGES; do 109 | parsePackgeTuple "$ARGS" 110 | 111 | if [ "$RUN_CARGO_TEST" = "false" ]; then 112 | DO_RUN_CARGO_TEST="false" 113 | fi 114 | 115 | if [ "$RUN_CARGO_TEST_WITH_COVERAGE" = "false" ]; then 116 | EXPECTED_COVERAGE="" 117 | fi 118 | 119 | cd "$PROJECT_ROOT/$PACKAGE" 120 | 121 | if [ -n "$EXPECTED_COVERAGE" ]; then 122 | printf "\n\n--== Unit Tests (With Coverage) for $PACKAGE ==--\n\n" 123 | 124 | TMP_COVERAGE_DIR="$COVERAGE_DIR/$PACKAGE" 125 | TMP_DOCTESTBINS_DIR="$TMP_COVERAGE_DIR/doctestbins" 126 | 127 | rm -rf "$TMP_COVERAGE_DIR" 128 | mkdir -p "$TMP_COVERAGE_DIR" 129 | 130 | rm -rf "$TMP_DOCTESTBINS_DIR" 131 | mkdir -p "$TMP_DOCTESTBINS_DIR" 132 | 133 | # Execute the tests 134 | if [ "$INCLUDE_DOC_TESTS" = "true" ]; then 135 | CARGO="cargo +nightly" 136 | 137 | LLVM_PROFILE_FILE="$TMP_COVERAGE_DIR/default_%m_%p.profraw" \ 138 | RUSTFLAGS="-D warnings -C instrument-coverage" \ 139 | RUSTDOCFLAGS="-D warnings -C instrument-coverage -Z unstable-options --persist-doctests $TMP_DOCTESTBINS_DIR" \ 140 | $CARGO test \ 141 | $CARGO_EXTRA_ARGS 142 | else 143 | CARGO="cargo" 144 | 145 | LLVM_PROFILE_FILE="$TMP_COVERAGE_DIR/default_%m_%p.profraw" \ 146 | RUSTFLAGS="-D warnings -C instrument-coverage" \ 147 | $CARGO test \ 148 | $CARGO_EXTRA_ARGS 149 | fi 150 | 151 | # Merge profdata 152 | $CARGO profdata -- \ 153 | merge \ 154 | -sparse "$TMP_COVERAGE_DIR"/default_*.profraw \ 155 | -o "$TMP_COVERAGE_DIR/merged.profdata" 156 | 157 | # Get all binary artifacts 158 | FILES_TESTS=$( \ 159 | RUSTFLAGS="-D warnings -C instrument-coverage" \ 160 | $CARGO test $CARGO_EXTRA_ARGS --tests --no-run --message-format=json \ 161 | | jq -r "select(.profile.test == true) | .filenames[]" \ 162 | | grep -v dSYM - \ 163 | ) 164 | FILES_DOC_TESTS=$(find "$TMP_DOCTESTBINS_DIR" -type f) 165 | FILES="$FILES_TESTS $FILES_DOC_TESTS" 166 | 167 | # Generate the report 168 | IGNORE=$(find "$PROJECT_ROOT" -mindepth 1 -maxdepth 1 -type d -not -name "$PACKAGE") 169 | $CARGO cov -- \ 170 | report \ 171 | --use-color \ 172 | --show-branch-summary=false \ 173 | --ignore-filename-regex='rustc/' \ 174 | --ignore-filename-regex='cargo/git' \ 175 | --ignore-filename-regex='cargo/registry' \ 176 | --ignore-filename-regex="$PROJECT_ROOT/$PACKAGE/tests" \ 177 | $( for DIR in $IGNORE; do printf " %s=%s/" --ignore-filename-regex $DIR; done ) \ 178 | --instr-profile="$TMP_COVERAGE_DIR/merged.profdata" \ 179 | $( for FILE in $FILES; do printf " %s %s" -object $FILE; done ) 180 | 181 | # Generate HTML report 182 | if [ "$GENERATE_COVERAGE_REPORT" = "true" ]; then 183 | $CARGO cov -- \ 184 | show \ 185 | --use-color \ 186 | --format=html \ 187 | --Xdemangler=rustfilt \ 188 | --show-instantiations=false \ 189 | --show-directory-coverage \ 190 | --show-line-counts-or-regions \ 191 | --ignore-filename-regex='rustc/' \ 192 | --ignore-filename-regex='cargo/git' \ 193 | --ignore-filename-regex='cargo/registry' \ 194 | --ignore-filename-regex='tests/' \ 195 | --output-dir="$TMP_COVERAGE_DIR/report" \ 196 | $( for DIR in $IGNORE; do printf " %s=%s/" --ignore-filename-regex $DIR; done ) \ 197 | --instr-profile="$TMP_COVERAGE_DIR/merged.profdata" \ 198 | $( for FILE in $FILES; do printf "%s %s " -object $FILE; done ) 199 | 2> /dev/null 200 | fi 201 | 202 | # Export coverage data 203 | $CARGO cov -- \ 204 | export \ 205 | --format=text \ 206 | --summary-only \ 207 | --ignore-filename-regex='rustc/' \ 208 | --ignore-filename-regex='cargo/git' \ 209 | --ignore-filename-regex='cargo/registry' \ 210 | --ignore-filename-regex="$PROJECT_ROOT/$PACKAGE/tests" \ 211 | $( for DIR in $IGNORE; do printf " %s=%s/" --ignore-filename-regex $DIR; done ) \ 212 | --instr-profile="$TMP_COVERAGE_DIR/merged.profdata" \ 213 | $( for FILE in $FILES; do printf " %s %s" -object $FILE; done ) \ 214 | > "$TMP_COVERAGE_DIR/coverage.json" \ 215 | 2> /dev/null 216 | COVERAGE_PERCENTAGE=$(cat "$TMP_COVERAGE_DIR/coverage.json" \ 217 | | jq '.data[0].totals.regions.percent') 218 | 219 | # Check test coverage 220 | if [ $(echo "$COVERAGE_PERCENTAGE < $EXPECTED_COVERAGE" | bc -l) = 1 ]; then 221 | echo "" 222 | echo "Test coverage for '$PACKAGE' is not fulfilled" 223 | echo " actual coverage: $COVERAGE_PERCENTAGE %" 224 | echo " expected coverage: $EXPECTED_COVERAGE %" 225 | exit 1 226 | fi 227 | elif [ "$DO_RUN_CARGO_TEST" = "true" ]; then 228 | printf "\n\n--== Unit Tests for $PACKAGE ==--\n\n" 229 | 230 | RUSTFLAGS="-D warnings" \ 231 | cargo test \ 232 | $CARGO_EXTRA_ARGS 233 | fi 234 | done 235 | -------------------------------------------------------------------------------- /.github/actions/deploy-dep-package/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Deploy DEB Package' 2 | inputs: 3 | host: 4 | description: 'Host to deploy the packages to' 5 | required: true 6 | repo: 7 | description: 'Repository to add the packages to' 8 | required: true 9 | codename: 10 | description: 'Codename to upload the package to' 11 | default: 'stable' 12 | required: false 13 | packages: 14 | description: 'List of packages to deploy' 15 | required: true 16 | package_dir: 17 | description: 'Directory the packages are stored on the host' 18 | required: true 19 | runs: 20 | using: 'composite' 21 | steps: 22 | - name: Deploy DEB Package 23 | shell: bash 24 | run: ${{ github.action_path }}/exec.sh 25 | env: 26 | DEPLOY_DEB_HOST: ${{ inputs.host }} 27 | DEPLOY_DEB_REPO: ${{ inputs.repo }} 28 | DEPLOY_DEB_CODENAME: ${{ inputs.codename }} 29 | DEPLOY_DEB_PACKAGES: ${{ inputs.packages }} 30 | DEPLOY_DEB_PACKAGE_DIR: ${{ inputs.package_dir }} 31 | -------------------------------------------------------------------------------- /.github/actions/deploy-dep-package/exec.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SCRIPT_DIR="$(dirname $0)" 6 | 7 | ssh -o BatchMode=yes "$DEPLOY_DEB_HOST" "rm -rf $DEPLOY_DEB_PACKAGE_DIR/incoming && mkdir -p $DEPLOY_DEB_PACKAGE_DIR/incoming" 8 | 9 | for PACKAGE in $DEPLOY_DEB_PACKAGES; do 10 | scp -o BatchMode=yes "$PACKAGE" "$DEPLOY_DEB_HOST:$DEPLOY_DEB_PACKAGE_DIR/incoming/" 11 | done 12 | 13 | ssh -o BatchMode=yes \ 14 | "$DEPLOY_DEB_HOST" \ 15 | "finish() { \ 16 | rm -rf \"$DEPLOY_DEB_PACKAGE_DIR/incoming\"; \ 17 | }; \ 18 | \ 19 | trap finish EXIT; \ 20 | \ 21 | cd \"$DEPLOY_DEB_PACKAGE_DIR/repos/$DEPLOY_DEB_REPO\" \ 22 | && reprepro -V includedeb $DEPLOY_DEB_CODENAME \"$DEPLOY_DEB_PACKAGE_DIR/incoming/\"*.deb" 23 | -------------------------------------------------------------------------------- /.github/actions/docker-build-and-push/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Docker Build and Push Image' 2 | description: 'Build and Push an Docker Image' 3 | inputs: 4 | name: 5 | description: 'Name of the image' 6 | required: true 7 | version: 8 | description: 'Version of the image' 9 | required: true 10 | username: 11 | description: 'Username to use for authentication' 12 | required: true 13 | password: 14 | description: 'Password to use for authentication' 15 | required: true 16 | context: 17 | description: 'Docker build context' 18 | required: false 19 | default: . 20 | registry: 21 | description: 'Docker registry to push to' 22 | required: false 23 | default: ghcr.io 24 | runs: 25 | using: 'composite' 26 | steps: 27 | - name: Log in to the Container registry 28 | uses: docker/login-action@v3.3.0 29 | with: 30 | registry: ${{ inputs.registry }} 31 | username: ${{ inputs.username }} 32 | password: ${{ inputs.password }} 33 | - name: Build and push docker image 34 | id: push 35 | uses: docker/build-push-action@v6.6.1 36 | with: 37 | context: ${{ inputs.context }} 38 | push: true 39 | tags: ${{ inputs.registry }}/${{ inputs.name }}:${{ inputs.version }},${{ inputs.registry }}/${{ inputs.name }}:latest 40 | -------------------------------------------------------------------------------- /.github/actions/docker-run/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Docker Run' 2 | description: 'Run a command in a new container' 3 | inputs: 4 | image: 5 | description: 'Image' 6 | required: true 7 | user: 8 | description: 'Run the container as the specified user' 9 | required: false 10 | options: 11 | description: 'Options' 12 | required: false 13 | run: 14 | description: 'Run command in container' 15 | required: false 16 | shell: 17 | description: 'Use a specific shell' 18 | required: false 19 | default: sh 20 | registry: 21 | description: 'Registry' 22 | required: false 23 | default: ghcr.io 24 | username: 25 | description: 'Username' 26 | required: false 27 | password: 28 | description: 'Password' 29 | required: false 30 | docker_network: 31 | description: 'Docker Network ID' 32 | default: ${{ job.container.network }} 33 | required: false 34 | volumes: 35 | description: 'Volumes that should be used for the container' 36 | required: false 37 | setup_known_hosts: 38 | description: 'Add gitlab.com to the list of known hosts' 39 | required: false 40 | default: true 41 | ssh_keys: 42 | description: 'List of base64 encoded keys to add to the SSH agent' 43 | required: false 44 | runs: 45 | using: 'composite' 46 | steps: 47 | - name: Run Docker Container 48 | shell: bash 49 | run: ${{ github.action_path }}/exec.sh 50 | env: 51 | DOCKER_RUN_IMAGE: ${{ inputs.image }} 52 | DOCKER_RUN_USER: ${{ inputs.user }} 53 | DOCKER_RUN_OPTIONS: ${{ inputs.options }} 54 | DOCKER_RUN_RUN: ${{ inputs.run }} 55 | DOCKER_RUN_SHELL: ${{ inputs.shell }} 56 | DOCKER_RUN_REGISTRY: ${{ inputs.registry }} 57 | DOCKER_RUN_USERNAME: ${{ inputs.username }} 58 | DOCKER_RUN_PASSWORD: ${{ inputs.password }} 59 | DOCKER_RUN_DOCKER_NETWORK: ${{ inputs.docker_network }} 60 | DOCKER_RUN_VOLUMES: ${{ inputs.volumes }} 61 | DOCKER_RUN_SSH_KEYS: ${{ inputs.ssh_keys }} 62 | DOCKER_RUN_SETUP_KNOWN_HOSTS: ${{ inputs.setup_known_hosts }} 63 | -------------------------------------------------------------------------------- /.github/actions/docker-run/exec.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SCRIPT="true" 6 | EXTRA_ARGS=() 7 | 8 | # Login to docker registry 9 | if [ ! -z $DOCKER_RUN_USERNAME ]; then 10 | echo "$DOCKER_RUN_PASSWORD" | docker login "$DOCKER_RUN_REGISTRY" -u "$DOCKER_RUN_USERNAME" --password-stdin 11 | fi 12 | 13 | # Pull the image 14 | docker pull "$DOCKER_RUN_IMAGE" 15 | 16 | # Join the specified docker network 17 | if [ ! -z $DOCKER_RUN_DOCKER_NETWORK ]; then 18 | EXTRA_ARGS+=(--network "$DOCKER_RUN_DOCKER_NETWORK") 19 | fi 20 | 21 | # Use the specified user 22 | if [ ! -z $DOCKER_RUN_USER ]; then 23 | USER_DIR="$RUNNER_TEMP/_home_$DOCKER_RUN_USER" 24 | 25 | USER_ID=$(id -u) 26 | GROUP_ID=$(id -g) 27 | 28 | mkdir -p "$USER_DIR" 29 | chown -R $USER_ID:$GROUP_ID "$USER_DIR" 30 | 31 | EXTRA_ARGS+=( \ 32 | --user $USER_ID:$GROUP_ID \ 33 | -v "$USER_DIR":"/home/$DOCKER_RUN_USER" \ 34 | ) 35 | 36 | for GROUP in $(id -G); do 37 | EXTRA_ARGS+=(--group-add "$GROUP") 38 | done 39 | 40 | SCRIPT="$SCRIPT; export HOME=/home/$DOCKER_RUN_USER" 41 | fi 42 | 43 | # Setup known hosts 44 | if [ "$DOCKER_RUN_SETUP_KNOWN_HOSTS" == "true" ]; then 45 | SCRIPT="$SCRIPT; mkdir -p ~/.ssh; ssh-keyscan -H github.com >> ~/.ssh/known_hosts" 46 | fi 47 | 48 | # Parse passed keys 49 | if [ ! -z "$DOCKER_RUN_SSH_KEYS" ]; then 50 | SCRIPT="$SCRIPT; eval \"\$(ssh-agent -s)\"" 51 | while IFS= read -r KEY; do 52 | if [ ! -z "$KEY" ]; then 53 | SCRIPT="$SCRIPT; echo \"$KEY\" | base64 -d | ssh-add -" 54 | fi 55 | done <<< "$DOCKER_RUN_SSH_KEYS" 56 | fi 57 | 58 | # Parse the passed volumes 59 | while IFS= read -r VOLUME; do 60 | IFS=":" read -ra PARTS <<< "$VOLUME" 61 | 62 | if [ "${#PARTS[@]}" != "2" ]; then 63 | continue; 64 | fi 65 | 66 | VOLUME_NAME="$HOSTNAME-${PARTS[0]}" 67 | VOLUME_PATH="${PARTS[1]}" 68 | 69 | if ! docker volume ls --format '{{.Name}}' | grep -q "^${VOLUME_NAME}$"; then 70 | echo "Create docker volume: $VOLUME_NAME" 71 | 72 | docker volume create "$VOLUME_NAME" 73 | else 74 | echo "Reuse existing docker volume: $VOLUME_NAME" 75 | fi 76 | 77 | EXTRA_ARGS+=(-v "$VOLUME_NAME":"$VOLUME_PATH") 78 | done <<< "$DOCKER_RUN_VOLUMES" 79 | 80 | # Set environment variables 81 | for ENV in $(export -p | cut -d' ' -f3 | cut -d'=' -f1 | grep -vE '^(OLDPWD|PATH|PWD|SHL|HOME|HOSTNAME|INPUT_.*|DOCKER_RUN_.*)$'); do 82 | EXTRA_ARGS+=(-e "$ENV") 83 | done 84 | 85 | # Bring up the container and execute the requested command 86 | SCRIPT="$SCRIPT; $DOCKER_RUN_RUN" 87 | 88 | exec docker run \ 89 | --rm \ 90 | ${EXTRA_ARGS[@]} \ 91 | -v "/var/run/docker.sock":"/var/run/docker.sock" \ 92 | -v "$GITHUB_WORKSPACE":"/github/workspace" \ 93 | --workdir /github/workspace \ 94 | --entrypoint="$DOCKER_RUN_SHELL" \ 95 | "$DOCKER_RUN_IMAGE" \ 96 | -c "$SCRIPT" 97 | -------------------------------------------------------------------------------- /.github/actions/licenses: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | SCRIPT_DIR="$(dirname $0)" 6 | SCRIPT_NAME="$(basename $0)" 7 | PROJECT_ROOT="$PROJECT_ROOT" 8 | 9 | if [ -t 2 ]; then 10 | RED=`tput setaf 1 || echo ""` 11 | YELLOW=`tput setaf 3 || echo ""` 12 | BOLD=`tput bold || echo ""` 13 | RESET=`tput sgr0 || echo ""` 14 | fi 15 | 16 | warn() { 17 | >&2 echo "${YELLOW}${BOLD}WARN${RESET}: $@" 18 | } 19 | 20 | fail() { 21 | >&2 echo "${RED}${BOLD}ERROR${RESET}: $@" 22 | exit 1 23 | } 24 | 25 | printHelp() { 26 | cat << EOF | fold -s -w $(tput cols) 27 | This is a simple script to update or check the licenses used of the different 28 | dependencies of a project. 29 | 30 | The general idea is that we use the \`License.lock\` file to create an appoved 31 | list of licences that is then checked by a suitable CI pipeline. 32 | 33 | Usage: 34 | $SCRIPT_NAME [command] 35 | 36 | Commands: 37 | update Extract the license information from the current 38 | dependencies and store them in the \`License.lock\` 39 | file. 40 | 41 | During the update the list of whitelisted and blacklisted 42 | licenses is checked. The dependency must have at least 43 | one license from the whilelist and must not have a 44 | license from the blacklist. 45 | 46 | Also a warning will be raised if the licenses of a crate 47 | have been changed (this normally means that the license 48 | situation of a crate has changed or will change in the 49 | future). 50 | 51 | check Check if the license information of the current dependencies 52 | matches the expected information from the \`License.lock\` 53 | file. 54 | 55 | help Print this help. 56 | 57 | Environment Variables: 58 | LICENSES_WHITELIST List of allowed licenses (currently $LICENSES_WHITELIST) 59 | LICENSES_BLACKLIST List of forbidden licenses (currently $LICENSES_BLACKLIST) 60 | IGNORE List of crates that are ignored during the check (Currently $IGNORE) 61 | MANIFEST_PATH Path of the \`Cargo.toml\` file (currenty $MANIFEST_PATH) 62 | LICENSE_LOCK_PATH Path of the \`License.lock\` file (currenty $LICENSE_LOCK_PATH) 63 | TARGET_DIR path of the target directory (currenty $TARGET_DIR) 64 | EOF 65 | } 66 | 67 | isWhitelisted() { 68 | local LICENSE 69 | 70 | for LICENSE in $LICENSES_WHITELIST; do 71 | if [ "$1" == "$LICENSE" ]; then 72 | return 0 73 | fi 74 | done 75 | 76 | return 1 77 | } 78 | 79 | isBlacklisted() { 80 | local LICENSE 81 | 82 | for LICENSE in $LICENSES_BLACKLIST; do 83 | if [ "$1" == "$LICENSE" ]; then 84 | return 0 85 | fi 86 | done 87 | 88 | return 1 89 | } 90 | 91 | isIgnored() { 92 | local CRATE 93 | 94 | if [[ "$1" == ppnp-* ]]; then 95 | return 0 96 | fi 97 | 98 | for CRATE in $IGNORE; do 99 | if [ "$1" == "$CRATE" ]; then 100 | return 0 101 | fi 102 | done 103 | 104 | return 1 105 | } 106 | 107 | loadCurrentLicences() { 108 | cargo license \ 109 | --gitlab \ 110 | --all-features \ 111 | --manifest-path "$MANIFEST_PATH" \ 112 | | jq '{ 113 | licenses: .licenses 114 | | sort_by(.id), 115 | dependencies: .dependencies 116 | | group_by(.name) 117 | | map({ 118 | name: .[0].name, 119 | versions: map(.version) 120 | | unique 121 | | sort, 122 | licenses: map(.licenses[]) 123 | | unique 124 | | sort 125 | }) 126 | | sort_by(.name) 127 | }' \ 128 | > "$TMP_LOCK_PATH" 129 | 130 | cat "$TMP_LOCK_PATH" | jq -cr '.dependencies[] | @sh "NAME=\(.name) LICENSES=(\(.licenses))"' 131 | } 132 | 133 | loadKnownLicenses() { 134 | while read -r ITEM; do 135 | eval "$ITEM" 136 | 137 | KNOWN_LICENSES[$NAME]=$LICENSES 138 | done < <(cat "$LICENSE_LOCK_PATH" | jq -cr '.dependencies[] | @sh "NAME=\(.name) LICENSES=\(.licenses | join(","))"') 139 | } 140 | 141 | checkLicences() { 142 | local NAME="$1" 143 | shift 144 | local LICENSE; 145 | local LICENSES=("$@") 146 | local TMP="false" 147 | 148 | for LICENSE in "${LICENSES[@]}"; do 149 | if isBlacklisted "$LICENSE"; then 150 | fail "$BOLD$NAME$RESET has a blacklisted license: $LICENSE" 151 | fi 152 | 153 | if isWhitelisted "$LICENSE"; then 154 | TMP="true" 155 | fi 156 | done 157 | 158 | if ! $TMP; then 159 | fail "$BOLD$NAME$RESET does not have a whitelisted license: ${LICENSES[@]}" 160 | fi 161 | } 162 | 163 | 164 | # Find the project root if it was not already set 165 | if [ -z "$PROJECT_ROOT" ]; then 166 | PROJECT_ROOT="$(readlink -f "$SCRIPT_DIR")" 167 | while [ ! -f "$PROJECT_ROOT/Cargo.lock" ]; do 168 | PROJECT_ROOT="$(readlink -f "$PROJECT_ROOT/..")" 169 | 170 | if [ -z "$PROJECT_ROOT" ]; then 171 | fail "Unable to find project root" 172 | fi 173 | done 174 | fi 175 | 176 | # Get and check the needed paths 177 | MANIFEST_PATH="${MANIFEST_PATH:-$PROJECT_ROOT/Cargo.toml}" 178 | LICENSE_LOCK_PATH="${LICENSE_LOCK_PATH:-$PROJECT_ROOT/License.lock}" 179 | TARGET_DIR="${CARGO_TARGET_DIR:-$PROJECT_ROOT/target}" 180 | TMP_LOCK_PATH="$TARGET_DIR/License.lock" 181 | 182 | if [ ! -f "$MANIFEST_PATH" ]; then 183 | fail "Project manifest was not found at '$MANIFEST_PATH'" 184 | fi 185 | 186 | mkdir -p "$TARGET_DIR" 187 | 188 | # Handle the command 189 | case "$1" in 190 | update) 191 | mkdir -p "$TARGET_DIR" 192 | 193 | declare -A KNOWN_LICENSES 194 | if [ -f "$LICENSE_LOCK_PATH" ]; then 195 | loadKnownLicenses 196 | fi 197 | 198 | # Check the licenses of the 199 | while read -r ITEM; do 200 | eval "$ITEM" 201 | 202 | if isIgnored "$NAME"; then 203 | continue 204 | fi 205 | 206 | checkLicences "$NAME" "${LICENSES[@]}" 207 | 208 | ACTUAL=$(IFS=','; printf "%s" "${LICENSES[*]}") 209 | EXPECTED="${KNOWN_LICENSES[$NAME]:-unknown}" 210 | if [ "$ACTUAL" != "$EXPECTED" ]; then 211 | warn "Licenses for $BOLD$NAME$RESET were unknown or have changed (new=$ACTUAL, old=$EXPECTED)!" 212 | fi 213 | done < <(loadCurrentLicences) 214 | 215 | mv "$TMP_LOCK_PATH" "$LICENSE_LOCK_PATH" 216 | ;; 217 | 218 | check) 219 | # Check if License.lock exsists 220 | if [ ! -f "$LICENSE_LOCK_PATH" ]; then 221 | fail "License lock file was not found at '$LICENSE_LOCK_PATH'" 222 | fi 223 | 224 | # Collect the licenses from License.lock 225 | declare -A KNOWN_LICENSES 226 | loadKnownLicenses 227 | 228 | # Check the actual licenses 229 | while read -r ITEM; do 230 | eval "$ITEM" 231 | 232 | if isIgnored "$NAME"; then 233 | continue 234 | fi 235 | 236 | checkLicences "$NAME" "${LICENSES[@]}" 237 | 238 | ACTUAL=$(IFS=','; printf "%s" "${LICENSES[*]}") 239 | EXPECTED="${KNOWN_LICENSES[$NAME]:-unknown}" 240 | if [ "$ACTUAL" != "$EXPECTED" ]; then 241 | fail "Licenses for $BOLD$NAME$RESET are unknown or have changed ($ACTUAL != $EXPECTED)!" 242 | fi 243 | done < <(loadCurrentLicences) 244 | ;; 245 | 246 | help|-h|-?|--help) 247 | printHelp 248 | exit 0 249 | ;; 250 | 251 | *) 252 | fail "Unknown command: '$1'" 253 | ;; 254 | esac 255 | -------------------------------------------------------------------------------- /.github/actions/minio-client/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Minio Client' 2 | inputs: 3 | run: 4 | description: 'Run command in container' 5 | required: true 6 | host: 7 | description: 'Minio endpoint of object storage host' 8 | required: true 9 | access_key: 10 | description: 'Minio access key (username)' 11 | required: true 12 | secret_key: 13 | description: 'Minio secret key (password)' 14 | required: true 15 | alias: 16 | description: 'Name of the host alias' 17 | required: false 18 | default: host 19 | runs: 20 | using: 'composite' 21 | steps: 22 | - name: Run MinIo Docker Container 23 | uses: ./.github/actions/docker-run 24 | env: 25 | MINIO_HOST: ${{ inputs.host }} 26 | MINIO_ACCESS_KEY: ${{ inputs.access_key }} 27 | MINIO_SECRET_KEY: ${{ inputs.secret_key }} 28 | MINIO_ALIAS: ${{ inputs.alias }} 29 | MINIO_RUN: ${{ inputs.run }} 30 | with: 31 | image: minio/mc:RELEASE.2024-07-31T15-58-33Z 32 | username: ${{ github.actor }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | user: user 35 | setup_known_hosts: false 36 | run: ./.github/actions/minio-client/exec.sh 37 | -------------------------------------------------------------------------------- /.github/actions/minio-client/exec.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | mc alias set $MINIO_ALIAS $MINIO_HOST $MINIO_ACCESS_KEY $MINIO_SECRET_KEY 6 | 7 | eval "$MINIO_RUN" 8 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check Workflow 2 | on: 3 | workflow_call: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | jobs: 9 | check: 10 | runs-on: self-hosted 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | clean: false 16 | 17 | - name: Cleanup 18 | run: | 19 | git clean -ffdx -e target/ 20 | git reset --hard HEAD 21 | 22 | - name: Check 23 | uses: ./.github/actions/docker-run 24 | with: 25 | image: ghcr.io/peeriot/rust:1.79.0-2 26 | username: ${{ github.actor }} 27 | password: ${{ secrets.GITHUB_TOKEN }} 28 | user: user 29 | volumes: | 30 | cache-cargo:/usr/local/cargo 31 | cache-rustup:/usr/local/rustup 32 | run: | 33 | ./.ci/check/check 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .vscode 3 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - check 3 | 4 | workflow: 5 | rules: 6 | - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_OPEN_MERGE_REQUESTS' 7 | when: always 8 | - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "master"' 9 | when: always 10 | - if: '$CI_COMMIT_BRANCH' 11 | when: never 12 | 13 | include: 14 | - '.ci/check/.gitlab-ci.yml' 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "murf", 5 | "murf-macros" 6 | ] 7 | 8 | [patch.crates-io] 9 | murf = { path = "murf" } 10 | murf-macros = { path = "murf-macros" } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /murf-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "murf-macros" 3 | version = "0.2.0" 4 | edition = "2021" 5 | description = "Murf's proc macros" 6 | license = "MIT" 7 | homepage = "https://github.com/peeriot/murf" 8 | readme = "README.md" 9 | keywords = [ "mock", "mocking", "test", "testing" ] 10 | categories = [ "development-tools::testing" ] 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [features] 16 | default = [] 17 | debug = [] 18 | debug-to-file = [] 19 | force-name = [] 20 | 21 | [dependencies] 22 | convert_case = "0.6" 23 | lazy_static = "1.4" 24 | proc-macro-crate = "3.1" 25 | proc-macro2 = { version = "1.0", features = [ "span-locations" ] } 26 | quote = "1.0" 27 | regex = "1.7" 28 | sha1 = "0.10" 29 | syn = { version = "2.0", features = [ "extra-traits", "full" ] } 30 | -------------------------------------------------------------------------------- /murf-macros/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /murf-macros/README.md: -------------------------------------------------------------------------------- 1 | # Murf Macros 2 | 3 | Procedural macros for use with `murf` 4 | 5 | ## License 6 | 7 | This project is licensed under the [MIT license](https://choosealicense.com/licenses/mit/). 8 | -------------------------------------------------------------------------------- /murf-macros/src/expect_call.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use proc_macro2::{Ident, TokenStream}; 4 | use quote::{quote, ToTokens}; 5 | use syn::{ 6 | parenthesized, 7 | parse::{Parse, ParseStream}, 8 | parse2, 9 | punctuated::Punctuated, 10 | token::{Comma, Gt, Lt, PathSep}, 11 | AngleBracketedGenericArguments, Expr, GenericArgument, Path, PathArguments, 12 | Result as ParseResult, Token, Type, 13 | }; 14 | 15 | use crate::misc::{format_expect_call, ident_murf, IterEx}; 16 | 17 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 18 | pub(crate) enum CallMode { 19 | Method, 20 | Static, 21 | } 22 | 23 | pub(crate) fn exec(input: TokenStream, mode: CallMode) -> TokenStream { 24 | let mut call: Call = match parse2(input) { 25 | Ok(mock) => mock, 26 | Err(err) => { 27 | return err.to_compile_error(); 28 | } 29 | }; 30 | call.mode = mode; 31 | 32 | call.into_token_stream() 33 | } 34 | 35 | struct Call { 36 | obj: Box, 37 | as_trait: Option, 38 | method: Ident, 39 | generics: Punctuated, 40 | args: Punctuated, 41 | mode: CallMode, 42 | } 43 | 44 | impl Parse for Call { 45 | fn parse(input: ParseStream<'_>) -> ParseResult { 46 | let obj = input.parse()?; 47 | 48 | let (obj, as_trait) = if let Expr::Cast(o) = obj { 49 | if let Type::Path(as_trait) = *o.ty { 50 | (o.expr, Some(as_trait.path)) 51 | } else { 52 | return Err(input.error("Expect trait path")); 53 | } 54 | } else { 55 | (Box::new(obj), None) 56 | }; 57 | 58 | input.parse::()?; 59 | let method = input.parse::()?; 60 | let generics = if input.peek(Token![::]) { 61 | AngleBracketedGenericArguments::parse_turbofish(input)?.args 62 | } else { 63 | Punctuated::default() 64 | }; 65 | let content; 66 | parenthesized!(content in input); 67 | let args = content.parse_terminated(Expr::parse, Token![,])?; 68 | 69 | Ok(Self { 70 | obj, 71 | as_trait, 72 | method, 73 | generics, 74 | args, 75 | mode: CallMode::Static, 76 | }) 77 | } 78 | } 79 | 80 | impl ToTokens for Call { 81 | fn to_tokens(&self, tokens: &mut TokenStream) { 82 | let Self { 83 | obj, 84 | as_trait, 85 | method, 86 | generics, 87 | args, 88 | mode, 89 | } = self; 90 | 91 | let ident_murf = ident_murf(); 92 | 93 | let desc = quote!(format!("at {}:{}", file!(), line!())); 94 | let obj = obj.to_token_stream(); 95 | let method = format_expect_call(method, as_trait.as_ref()); 96 | let generics = as_trait 97 | .as_ref() 98 | .and_then(|t| t.segments.last()) 99 | .and_then(|s| { 100 | if let PathArguments::AngleBracketed(a) = &s.arguments { 101 | Some(a.args.clone()) 102 | } else { 103 | None 104 | } 105 | }) 106 | .into_iter() 107 | .flatten() 108 | .chain(generics.iter().cloned()) 109 | .collect::>(); 110 | let turbofish = if generics.is_empty() { 111 | None 112 | } else { 113 | Some(AngleBracketedGenericArguments { 114 | colon2_token: Some(PathSep::default()), 115 | lt_token: Lt::default(), 116 | args: generics, 117 | gt_token: Gt::default(), 118 | }) 119 | }; 120 | let args = if args.is_empty() && mode == &CallMode::Static { 121 | quote!(.with(#ident_murf :: matcher::no_args())) 122 | } else { 123 | let call_method = mode == &CallMode::Method; 124 | let args = args.iter().map(|a| { 125 | if a.to_token_stream().to_string() == "_" { 126 | Cow::Owned(Expr::Verbatim(quote!(#ident_murf :: matcher::any()))) 127 | } else { 128 | Cow::Borrowed(a) 129 | } 130 | }); 131 | 132 | let mut arg_count = 0; 133 | let args = call_method 134 | .then(|| Cow::Owned(Expr::Verbatim(quote!(#ident_murf :: matcher::any())))) 135 | .into_iter() 136 | .chain(args) 137 | .inspect(|_| arg_count += 1) 138 | .parenthesis(); 139 | 140 | if arg_count > 1 { 141 | quote!(.with(#ident_murf :: matcher::multi(#args))) 142 | } else { 143 | quote!(.with(#args)) 144 | } 145 | }; 146 | 147 | tokens.extend(quote! { 148 | #obj.mock_handle().#method #turbofish().description(#desc)#args 149 | }); 150 | 151 | #[cfg(feature = "debug")] 152 | println!("\nexpect_call!:\n{tokens:#}\n"); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /murf-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn( 2 | clippy::pedantic, 3 | future_incompatible, 4 | missing_debug_implementations, 5 | missing_docs, 6 | nonstandard_style, 7 | rust_2018_idioms, 8 | rust_2021_compatibility, 9 | unused 10 | )] 11 | #![allow( 12 | clippy::module_name_repetitions, 13 | clippy::no_effect_underscore_binding, 14 | clippy::similar_names 15 | )] 16 | #![doc = include_str!("../README.md")] 17 | #![cfg_attr(feature = "debug-to-file", feature(proc_macro_span))] 18 | 19 | use expect_call::CallMode; 20 | use proc_macro::TokenStream; 21 | 22 | mod expect_call; 23 | mod misc; 24 | mod mock; 25 | 26 | /// Macro to generate a mockable version of a type or trait. 27 | /// 28 | /// # Example 29 | /// 30 | /// The following example will generate a mocked version of `MyStruct` that 31 | /// implements the `Fuu` trait. 32 | /// 33 | /// ``` 34 | /// trait Fuu { 35 | /// fn fuu(&self) -> usize; 36 | /// } 37 | /// 38 | /// mock! { 39 | /// #[derive(Default)] 40 | /// pub struct MyStruct; 41 | /// 42 | /// impl Fuu for MyStruct { 43 | /// fn fuu(&self) -> usize; 44 | /// } 45 | /// } 46 | /// 47 | /// let (handle, mock) = MyStruct::mock_with_handle(); 48 | /// 49 | /// expect_method_call!(handle as Fuu, fuu()).will_once(Return(1)); 50 | /// 51 | /// assert_eq!(1, mock.fuu()); 52 | /// ``` 53 | #[proc_macro] 54 | #[cfg(not(doctest))] 55 | pub fn mock(input: TokenStream) -> TokenStream { 56 | mock::exec(input.into()).into() 57 | } 58 | 59 | /// Helper macro to define an call expectation of a specific function. 60 | /// 61 | /// # Example 62 | /// 63 | /// ``` 64 | /// let (handle, mock) = MyStruct::mock_with_handle(); 65 | /// 66 | /// expect_call!(handle as Fuu, fuu(_)).will_once(Return(1)); 67 | /// ``` 68 | #[proc_macro] 69 | #[cfg(not(doctest))] 70 | pub fn expect_call(input: TokenStream) -> TokenStream { 71 | expect_call::exec(input.into(), CallMode::Static).into() 72 | } 73 | 74 | /// Helper macro to define an call expectation of a specific method. Same as 75 | /// [`expect_call!`] but will automatically add a `any` matcher for the `self` 76 | /// argument. 77 | /// 78 | /// # Example 79 | /// 80 | /// ``` 81 | /// let (handle, mock) = MyStruct::mock_with_handle(); 82 | /// 83 | /// expect_method_call!(handle as Fuu, fuu()).will_once(Return(1)); 84 | /// ``` 85 | #[proc_macro] 86 | #[cfg(not(doctest))] 87 | pub fn expect_method_call(input: TokenStream) -> TokenStream { 88 | expect_call::exec(input.into(), CallMode::Method).into() 89 | } 90 | -------------------------------------------------------------------------------- /murf-macros/src/misc/attribs_ex.rs: -------------------------------------------------------------------------------- 1 | use syn::{ 2 | parse::ParseStream, punctuated::Punctuated, token::Comma, Attribute, ImplItem, ImplItemFn, 3 | ItemEnum, ItemImpl, ItemStruct, Meta, Path, 4 | }; 5 | 6 | /*AttribsEx */ 7 | 8 | pub(crate) trait AttribsEx: Sized { 9 | fn derives(&self, ident: &str) -> bool { 10 | let _ident = ident; 11 | 12 | false 13 | } 14 | 15 | fn has_murf_attr(&self, ident: &str) -> bool { 16 | let _ident = ident; 17 | 18 | false 19 | } 20 | 21 | fn remove_murf_attrs(self) -> Self { 22 | self 23 | } 24 | } 25 | 26 | impl AttribsEx for Vec { 27 | fn derives(&self, ident: &str) -> bool { 28 | self.iter().any(|attr| match &attr.meta { 29 | Meta::List(ml) if attr.path().is_ident("derive") => { 30 | let mut ret = false; 31 | 32 | let _ = ml.parse_args_with(|p: ParseStream<'_>| { 33 | if let Ok(ml) = Punctuated::::parse_separated_nonempty(p) { 34 | for p in &ml { 35 | if p.is_ident(ident) { 36 | ret = true; 37 | } 38 | } 39 | } 40 | 41 | Ok(()) 42 | }); 43 | 44 | ret 45 | } 46 | _ => false, 47 | }) 48 | } 49 | 50 | fn has_murf_attr(&self, ident: &str) -> bool { 51 | self.iter().any(|attr| match &attr.meta { 52 | Meta::List(ml) if attr.path().is_ident("murf") => { 53 | let mut ret = false; 54 | 55 | let _ = ml.parse_args_with(|p: ParseStream<'_>| { 56 | if let Ok(ml) = Punctuated::::parse_separated_nonempty(p) { 57 | for p in &ml { 58 | if p.is_ident(ident) { 59 | ret = true; 60 | } 61 | } 62 | } 63 | 64 | Ok(()) 65 | }); 66 | 67 | ret 68 | } 69 | _ => false, 70 | }) 71 | } 72 | 73 | fn remove_murf_attrs(mut self) -> Self { 74 | self.retain(|a| !a.path().is_ident("murf")); 75 | 76 | self 77 | } 78 | } 79 | 80 | impl AttribsEx for ItemEnum { 81 | fn derives(&self, ident: &str) -> bool { 82 | self.attrs.derives(ident) 83 | } 84 | 85 | fn has_murf_attr(&self, ident: &str) -> bool { 86 | self.attrs.has_murf_attr(ident) 87 | } 88 | 89 | fn remove_murf_attrs(mut self) -> Self { 90 | self.attrs = self.attrs.remove_murf_attrs(); 91 | 92 | self 93 | } 94 | } 95 | 96 | impl AttribsEx for ItemStruct { 97 | fn derives(&self, ident: &str) -> bool { 98 | self.attrs.derives(ident) 99 | } 100 | 101 | fn has_murf_attr(&self, ident: &str) -> bool { 102 | self.attrs.has_murf_attr(ident) 103 | } 104 | 105 | fn remove_murf_attrs(mut self) -> Self { 106 | self.attrs = self.attrs.remove_murf_attrs(); 107 | 108 | self 109 | } 110 | } 111 | 112 | impl AttribsEx for ItemImpl { 113 | fn remove_murf_attrs(mut self) -> Self { 114 | self.attrs = self.attrs.remove_murf_attrs(); 115 | self.items = self 116 | .items 117 | .into_iter() 118 | .map(AttribsEx::remove_murf_attrs) 119 | .collect(); 120 | 121 | self 122 | } 123 | } 124 | 125 | impl AttribsEx for ImplItem { 126 | fn remove_murf_attrs(self) -> Self { 127 | match self { 128 | Self::Fn(x) => Self::Fn(x.remove_murf_attrs()), 129 | x => x, 130 | } 131 | } 132 | } 133 | 134 | impl AttribsEx for ImplItemFn { 135 | fn has_murf_attr(&self, ident: &str) -> bool { 136 | self.attrs.has_murf_attr(ident) 137 | } 138 | 139 | fn remove_murf_attrs(mut self) -> Self { 140 | self.attrs = self.attrs.remove_murf_attrs(); 141 | 142 | self 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /murf-macros/src/misc/formatted_string.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use quote::ToTokens; 3 | use regex::{Captures, Regex}; 4 | 5 | pub(crate) trait FormattedString { 6 | fn to_formatted_string(&self) -> String; 7 | } 8 | 9 | impl FormattedString for X 10 | where 11 | X: ToTokens, 12 | { 13 | fn to_formatted_string(&self) -> String { 14 | let code = self.to_token_stream().to_string(); 15 | let code = PATH_FORMAT_1.replace_all(&code, |c: &Captures<'_>| c[1].to_string()); 16 | let code = PATH_FORMAT_2.replace_all(&code, "&"); 17 | 18 | code.into_owned() 19 | } 20 | } 21 | 22 | lazy_static! { 23 | static ref PATH_FORMAT_1: Regex = Regex::new(r"\s*(<|>)\s*").unwrap(); 24 | static ref PATH_FORMAT_2: Regex = Regex::new(r"&\s*").unwrap(); 25 | } 26 | -------------------------------------------------------------------------------- /murf-macros/src/misc/generics_ex.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Span, TokenStream}; 2 | use quote::quote; 3 | use syn::{ 4 | punctuated::Punctuated, 5 | token::{Colon, Comma, Gt, Lt}, 6 | GenericArgument, GenericParam, Generics, Lifetime, LifetimeParam, PathArguments, Type, 7 | TypeParamBound, WherePredicate, 8 | }; 9 | 10 | use crate::misc::IterEx; 11 | 12 | use super::TypeEx; 13 | 14 | pub(crate) trait GenericsEx { 15 | fn get_lifetime_mut(&mut self, lt: &str) -> Option<&mut LifetimeParam>; 16 | 17 | fn add_lifetime(self, lt: &str) -> Self; 18 | fn add_lifetime_bounds(self, lt: &str) -> Self; 19 | 20 | fn remove_lifetimes(self, lts: &Punctuated) -> Self; 21 | fn remove_other(self, other: &Generics) -> Self; 22 | 23 | fn make_phantom_data(&self) -> TokenStream; 24 | 25 | fn merge(self, other: &Generics) -> Self; 26 | fn replace_self_type(self, type_: &Type, changed: &mut bool) -> Self; 27 | } 28 | 29 | impl GenericsEx for Generics { 30 | fn get_lifetime_mut(&mut self, lt: &str) -> Option<&mut LifetimeParam> { 31 | for x in &mut self.params { 32 | if let GenericParam::Lifetime(x) = x { 33 | if x.lifetime.to_string() == lt { 34 | return Some(x); 35 | } 36 | } 37 | } 38 | 39 | None 40 | } 41 | 42 | fn add_lifetime(mut self, lt: &str) -> Self { 43 | for x in &self.params { 44 | if matches!(x, GenericParam::Lifetime(x) if x.lifetime.to_string() == lt) { 45 | return self; 46 | } 47 | } 48 | 49 | if self.lt_token.is_none() { 50 | self.lt_token = Some(Lt::default()); 51 | } 52 | 53 | self.params.insert( 54 | 0, 55 | GenericParam::Lifetime(LifetimeParam::new(Lifetime::new(lt, Span::call_site()))), 56 | ); 57 | 58 | if self.gt_token.is_none() { 59 | self.gt_token = Some(Gt::default()); 60 | } 61 | 62 | self 63 | } 64 | 65 | fn add_lifetime_bounds(mut self, lt: &str) -> Self { 66 | self.params.iter_mut().for_each(|param| match param { 67 | GenericParam::Type(t) => { 68 | if t.colon_token.is_none() { 69 | t.colon_token = Some(Colon::default()); 70 | } 71 | 72 | t.bounds.push(TypeParamBound::Lifetime(Lifetime::new( 73 | lt, 74 | Span::call_site(), 75 | ))); 76 | } 77 | GenericParam::Lifetime(t) if t.lifetime.ident != lt[1..] => { 78 | if t.colon_token.is_none() { 79 | t.colon_token = Some(Colon::default()); 80 | } 81 | 82 | t.bounds.push(Lifetime::new(lt, Span::call_site())); 83 | } 84 | _ => (), 85 | }); 86 | 87 | self 88 | } 89 | 90 | fn remove_lifetimes(mut self, lts: &Punctuated) -> Self { 91 | self.params = self 92 | .params 93 | .into_iter() 94 | .filter(|param| { 95 | if let GenericParam::Lifetime(p) = param { 96 | for lt in lts { 97 | if p.lifetime.ident == lt.ident { 98 | return false; 99 | } 100 | } 101 | } 102 | 103 | true 104 | }) 105 | .collect(); 106 | 107 | self 108 | } 109 | 110 | fn remove_other(mut self, other: &Generics) -> Self { 111 | self.params = self 112 | .params 113 | .into_iter() 114 | .filter(|param| { 115 | for p in &other.params { 116 | match (param, p) { 117 | (GenericParam::Type(a), GenericParam::Type(b)) if a.ident == b.ident => { 118 | return false 119 | } 120 | (GenericParam::Const(a), GenericParam::Const(b)) if a.ident == b.ident => { 121 | return false 122 | } 123 | (GenericParam::Lifetime(a), GenericParam::Lifetime(b)) 124 | if a.lifetime == b.lifetime => 125 | { 126 | return false 127 | } 128 | (_, _) => (), 129 | } 130 | } 131 | 132 | true 133 | }) 134 | .collect(); 135 | 136 | self 137 | } 138 | 139 | fn make_phantom_data(&self) -> TokenStream { 140 | let params = self 141 | .params 142 | .iter() 143 | .filter_map(|param| match param { 144 | GenericParam::Lifetime(lt) => { 145 | let lt = <.lifetime; 146 | 147 | Some(quote!(& #lt ())) 148 | } 149 | GenericParam::Type(ty) => { 150 | let ident = &ty.ident; 151 | 152 | Some(quote!(#ident)) 153 | } 154 | GenericParam::Const(_) => None, 155 | }) 156 | .parenthesis(); 157 | 158 | quote!(PhantomData<#params>) 159 | } 160 | 161 | fn merge(mut self, other: &Generics) -> Self { 162 | for p1 in &other.params { 163 | let mut merged = false; 164 | 165 | for p2 in &mut self.params { 166 | match (p1, p2) { 167 | (GenericParam::Type(p1), GenericParam::Type(p2)) if p1.ident == p2.ident => { 168 | p2.bounds.extend(p1.bounds.clone()); 169 | 170 | merged = true; 171 | break; 172 | } 173 | (GenericParam::Lifetime(p1), GenericParam::Lifetime(p2)) 174 | if p1.lifetime.ident == p2.lifetime.ident => 175 | { 176 | p2.bounds.extend(p1.bounds.clone()); 177 | 178 | merged = true; 179 | break; 180 | } 181 | (_, _) => (), 182 | } 183 | } 184 | 185 | if !merged { 186 | self.params.push(p1.clone()); 187 | } 188 | } 189 | 190 | self 191 | } 192 | 193 | fn replace_self_type(mut self, type_: &Type, changed: &mut bool) -> Self { 194 | let Some(where_clause) = &mut self.where_clause else { 195 | return self; 196 | }; 197 | 198 | for p in &mut where_clause.predicates { 199 | let WherePredicate::Type(t) = p else { 200 | continue; 201 | }; 202 | 203 | t.bounded_ty = t.bounded_ty.clone().replace_self_type(type_, changed); 204 | 205 | for b in &mut t.bounds { 206 | let TypeParamBound::Trait(t) = b else { 207 | continue; 208 | }; 209 | 210 | for s in &mut t.path.segments { 211 | let PathArguments::AngleBracketed(a) = &mut s.arguments else { 212 | continue; 213 | }; 214 | 215 | for a in &mut a.args { 216 | let GenericArgument::Type(t) = a else { 217 | continue; 218 | }; 219 | 220 | *t = t.clone().replace_self_type(type_, changed); 221 | } 222 | } 223 | } 224 | } 225 | 226 | self 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /murf-macros/src/misc/item_impl_ex.rs: -------------------------------------------------------------------------------- 1 | use std::mem::take; 2 | 3 | use syn::{punctuated::Punctuated, GenericParam, ItemImpl, WherePredicate}; 4 | 5 | use super::{TempLifetimes, TypeEx}; 6 | 7 | pub(crate) trait ItemImplEx: Sized { 8 | fn split_off_temp_lifetimes(self) -> (Self, TempLifetimes); 9 | } 10 | 11 | impl ItemImplEx for ItemImpl { 12 | fn split_off_temp_lifetimes(mut self) -> (Self, TempLifetimes) { 13 | let mut lts = Punctuated::default(); 14 | 15 | let params = take(&mut self.generics.params); 16 | 17 | for param in params { 18 | match param { 19 | GenericParam::Lifetime(lt) if !self.self_ty.contains_lifetime(<.lifetime) => { 20 | if let Some(wc) = &mut self.generics.where_clause { 21 | wc.predicates = wc.predicates.iter().filter_map(|p| { 22 | if matches!(p, WherePredicate::Lifetime(plt) if plt.lifetime == lt.lifetime) { 23 | None 24 | } else { 25 | Some(p.clone()) 26 | } 27 | }).collect(); 28 | } 29 | 30 | lts.push(lt.lifetime); 31 | } 32 | param => self.generics.params.push(param), 33 | } 34 | } 35 | 36 | if self.generics.params.is_empty() { 37 | self.generics.lt_token = None; 38 | self.generics.gt_token = None; 39 | } 40 | 41 | if self 42 | .generics 43 | .where_clause 44 | .as_ref() 45 | .is_some_and(|wc| wc.predicates.is_empty()) 46 | { 47 | self.generics.where_clause = None; 48 | } 49 | 50 | (self, TempLifetimes::new(lts)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /murf-macros/src/misc/iter_ex.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{quote, ToTokens}; 3 | 4 | pub(crate) trait IterEx { 5 | fn parenthesis(self) -> TokenStream; 6 | } 7 | 8 | impl IterEx for X 9 | where 10 | X: IntoIterator, 11 | X::Item: ToTokens, 12 | { 13 | fn parenthesis(self) -> TokenStream { 14 | let mut count = 0; 15 | let iter = self.into_iter().inspect(|_| count += 1); 16 | 17 | let ret = quote!(#( #iter ),*); 18 | 19 | match count { 20 | 0 => quote!(()), 21 | 1 => ret, 22 | _ => quote!((#ret)), 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /murf-macros/src/misc/method_ex.rs: -------------------------------------------------------------------------------- 1 | use quote::{quote, ToTokens}; 2 | use syn::{FnArg, ImplItemFn, Item, ReturnType, Stmt}; 3 | 4 | pub(crate) trait MethodEx { 5 | fn is_associated_fn(&self) -> bool; 6 | fn has_default_impl(&self) -> bool; 7 | fn need_default_impl(&self) -> bool; 8 | } 9 | 10 | impl MethodEx for ImplItemFn { 11 | fn is_associated_fn(&self) -> bool { 12 | self.sig.inputs.iter().all(|i| match i { 13 | FnArg::Receiver(_) => false, 14 | FnArg::Typed(t) if t.pat.to_token_stream().to_string() == "self" => false, 15 | FnArg::Typed(_) => true, 16 | }) 17 | } 18 | 19 | fn has_default_impl(&self) -> bool { 20 | let stmts = &self.block.stmts; 21 | 22 | let no_impl_block = self.block.stmts.len() == 1 23 | && matches!(self.block.stmts.last(), Some(Stmt::Item(Item::Verbatim(v))) if v.to_string() == ";"); 24 | let generated_panic_impl_block = quote!( #( #stmts )* ) 25 | .to_string() 26 | .contains("\"No default action specified!\""); 27 | 28 | !no_impl_block && !generated_panic_impl_block 29 | } 30 | 31 | fn need_default_impl(&self) -> bool { 32 | self.sig.output != ReturnType::Default 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /murf-macros/src/misc/mod.rs: -------------------------------------------------------------------------------- 1 | mod attribs_ex; 2 | mod formatted_string; 3 | mod generics_ex; 4 | mod item_impl_ex; 5 | mod iter_ex; 6 | mod method_ex; 7 | mod return_type_ex; 8 | mod temp_lifetimes; 9 | mod type_ex; 10 | 11 | use convert_case::{Case, Casing}; 12 | use proc_macro2::Ident; 13 | use quote::format_ident; 14 | use syn::Path; 15 | 16 | pub(crate) use attribs_ex::AttribsEx; 17 | pub(crate) use formatted_string::FormattedString; 18 | pub(crate) use generics_ex::GenericsEx; 19 | pub(crate) use item_impl_ex::ItemImplEx; 20 | pub(crate) use iter_ex::IterEx; 21 | pub(crate) use method_ex::MethodEx; 22 | pub(crate) use return_type_ex::ReturnTypeEx; 23 | pub(crate) use temp_lifetimes::TempLifetimes; 24 | pub(crate) use type_ex::{LifetimeReplaceMode, TypeEx}; 25 | 26 | pub(crate) fn format_expect_call(method: &Ident, as_trait: Option<&Path>) -> Ident { 27 | if let Some(t) = as_trait { 28 | format_ident!( 29 | "as_{}_expect_{}", 30 | t.segments 31 | .iter() 32 | .map(|s| s.ident.to_string()) 33 | .collect::>() 34 | .join("_") 35 | .replace(|c: char| !c.is_alphanumeric(), "_") 36 | .to_case(Case::Snake), 37 | method 38 | ) 39 | } else { 40 | format_ident!("expect_{}", method.to_string()) 41 | } 42 | } 43 | 44 | pub(crate) fn format_expect_module(method: &Ident, as_trait: Option<&Path>) -> Ident { 45 | if let Some(t) = as_trait { 46 | format_ident!( 47 | "mock_trait_{}_method_{}", 48 | t.segments 49 | .iter() 50 | .map(|s| s.ident.to_string()) 51 | .collect::>() 52 | .join("_") 53 | .replace(|c: char| !c.is_alphanumeric(), "_") 54 | .to_case(Case::Snake), 55 | method 56 | ) 57 | } else { 58 | format_ident!("mock_method_{}", method.to_string()) 59 | } 60 | } 61 | 62 | pub(crate) fn format_expectations_field(ident: &Ident) -> Ident { 63 | format_ident!("{}_expectations", ident) 64 | } 65 | 66 | #[cfg(feature = "force-name")] 67 | pub(crate) fn ident_murf() -> Ident { 68 | format_ident!("murf") 69 | } 70 | 71 | #[cfg(not(feature = "force-name"))] 72 | pub(crate) fn ident_murf() -> Ident { 73 | use proc_macro_crate::{crate_name, FoundCrate}; 74 | 75 | match crate_name("murf") { 76 | Ok(FoundCrate::Itself) => format_ident!("crate"), 77 | Ok(FoundCrate::Name(name)) => format_ident!("{name}"), 78 | Err(_) => format_ident!("murf"), 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /murf-macros/src/misc/return_type_ex.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use quote::quote; 3 | use syn::{Lifetime, ReturnType, Type}; 4 | 5 | use super::{type_ex::LifetimeReplaceMode, TypeEx}; 6 | 7 | pub(crate) trait ReturnTypeEx { 8 | fn to_action_return_type(&self, ty: &Type, need_mock_lt: &mut bool) -> Type; 9 | } 10 | 11 | impl ReturnTypeEx for ReturnType { 12 | fn to_action_return_type(&self, ty: &Type, need_mock_lt: &mut bool) -> Type { 13 | if let ReturnType::Type(_, t) = &self { 14 | let mut t = t 15 | .clone() 16 | .replace_self_type(ty, need_mock_lt) 17 | .replace_default_lifetime(LifetimeReplaceMode::Mock); 18 | if let Type::Reference(t) = &mut t { 19 | t.lifetime = Some(Lifetime::new("'mock", Span::call_site())); 20 | *need_mock_lt = true; 21 | } 22 | 23 | t 24 | } else { 25 | Type::Verbatim(quote!(())) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /murf-macros/src/misc/temp_lifetimes.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | use std::sync::atomic::{AtomicUsize, Ordering}; 3 | 4 | use proc_macro2::Span; 5 | use syn::{punctuated::Punctuated, token::Comma, Lifetime}; 6 | 7 | #[derive(Default, Debug, Clone)] 8 | pub(crate) struct TempLifetimes(pub Punctuated); 9 | 10 | impl TempLifetimes { 11 | pub(crate) fn new(lifetimes: Punctuated) -> Self { 12 | Self(lifetimes) 13 | } 14 | 15 | pub(crate) fn generate(&mut self) -> Lifetime { 16 | let id = NEXT.fetch_add(1, Ordering::Relaxed); 17 | 18 | let lt = format!("'murf_tmp_{id}"); 19 | let lt = Lifetime::new(<, Span::call_site()); 20 | 21 | self.0.push(lt.clone()); 22 | 23 | lt 24 | } 25 | } 26 | 27 | impl Deref for TempLifetimes { 28 | type Target = Punctuated; 29 | 30 | fn deref(&self) -> &Self::Target { 31 | &self.0 32 | } 33 | } 34 | 35 | static NEXT: AtomicUsize = AtomicUsize::new(0); 36 | -------------------------------------------------------------------------------- /murf-macros/src/misc/type_ex.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::UnsafeCell, 3 | ptr::{from_mut, from_ref}, 4 | }; 5 | 6 | use proc_macro2::{Ident, Span}; 7 | use syn::{GenericArgument, Lifetime, Path, PathArguments, ReturnType, Type, TypeParamBound}; 8 | 9 | use super::TempLifetimes; 10 | 11 | pub(crate) enum LifetimeReplaceMode<'x> { 12 | Mock, 13 | Temp(&'x mut TempLifetimes), 14 | } 15 | 16 | impl<'x> LifetimeReplaceMode<'x> { 17 | fn generate(&mut self) -> Lifetime { 18 | match self { 19 | Self::Mock => Lifetime::new("'mock", Span::call_site()), 20 | Self::Temp(tmp) => tmp.generate(), 21 | } 22 | } 23 | } 24 | 25 | pub(crate) trait TypeEx { 26 | fn contains_lifetime(&self, lt: &Lifetime) -> bool; 27 | fn contains_self_type(&self) -> bool; 28 | 29 | fn replace_self_type(self, type_: &Type, changed: &mut bool) -> Self; 30 | fn replace_default_lifetime(self, mode: LifetimeReplaceMode<'_>) -> Self; 31 | 32 | fn make_static(self) -> Self; 33 | } 34 | 35 | impl TypeEx for Type { 36 | fn contains_lifetime(&self, lt: &Lifetime) -> bool { 37 | struct Visitor<'a> { 38 | lt: &'a Lifetime, 39 | result: bool, 40 | } 41 | 42 | impl<'a> TypeVisitor for Visitor<'a> { 43 | fn visit_lifetime(&mut self, lt: &UnsafeCell) -> bool { 44 | let lt = unsafe { &*lt.get() }; 45 | self.result = self.lt.ident == lt.ident || self.result; 46 | 47 | !self.result 48 | } 49 | } 50 | 51 | let mut visitor = Visitor { lt, result: false }; 52 | 53 | visitor.visit(unsafe_cell_ref(self)); 54 | 55 | visitor.result 56 | } 57 | 58 | fn contains_self_type(&self) -> bool { 59 | struct Visitor { 60 | result: bool, 61 | } 62 | 63 | impl TypeVisitor for Visitor { 64 | fn visit_type(&mut self, ty: &UnsafeCell) -> bool { 65 | let ty = unsafe { &*ty.get() }; 66 | 67 | if let Type::Path(t) = ty { 68 | if t.path.segments.len() == 1 && t.path.segments[0].ident == "Self" { 69 | self.result = true; 70 | } 71 | } 72 | 73 | !self.result 74 | } 75 | } 76 | 77 | let mut visitor = Visitor { result: false }; 78 | 79 | visitor.visit(unsafe_cell_ref(self)); 80 | 81 | visitor.result 82 | } 83 | 84 | fn replace_self_type(mut self, type_: &Type, changed: &mut bool) -> Self { 85 | struct Visitor<'a> { 86 | type_: &'a Type, 87 | changed: &'a mut bool, 88 | } 89 | 90 | impl<'a> TypeVisitor for Visitor<'a> { 91 | fn visit_type(&mut self, ty: &UnsafeCell) -> bool { 92 | let ty = unsafe { &mut *ty.get() }; 93 | 94 | if let Type::Path(t) = ty { 95 | if t.path.segments.len() == 1 && t.path.segments[0].ident == "Self" { 96 | *ty = self.type_.clone(); 97 | *self.changed = true; 98 | } 99 | } 100 | 101 | true 102 | } 103 | } 104 | 105 | let mut visitor = Visitor { type_, changed }; 106 | 107 | visitor.visit(unsafe_cell_mut(&mut self)); 108 | 109 | self 110 | } 111 | 112 | fn replace_default_lifetime(mut self, mode: LifetimeReplaceMode<'_>) -> Self { 113 | struct Visitor<'a> { 114 | mode: LifetimeReplaceMode<'a>, 115 | } 116 | 117 | impl<'a> TypeVisitor for Visitor<'a> { 118 | fn visit_type(&mut self, ty: &UnsafeCell) -> bool { 119 | let ty = unsafe { &mut *ty.get() }; 120 | 121 | if let Type::Reference(r) = ty { 122 | if r.lifetime.is_none() { 123 | r.lifetime = Some(self.mode.generate()); 124 | } 125 | } 126 | 127 | true 128 | } 129 | 130 | fn visit_lifetime(&mut self, lt: &UnsafeCell) -> bool { 131 | let lt = unsafe { &mut *lt.get() }; 132 | 133 | if lt.ident == "_" { 134 | *lt = self.mode.generate(); 135 | } 136 | 137 | true 138 | } 139 | } 140 | 141 | let mut visitor = Visitor { mode }; 142 | 143 | visitor.visit(unsafe_cell_mut(&mut self)); 144 | 145 | self 146 | } 147 | 148 | fn make_static(mut self) -> Self { 149 | struct Visitor; 150 | 151 | impl TypeVisitor for Visitor { 152 | fn visit_type(&mut self, ty: &UnsafeCell) -> bool { 153 | let ty = unsafe { &mut *ty.get() }; 154 | 155 | match ty { 156 | Type::Path(ty) => { 157 | for seg in &mut ty.path.segments { 158 | match &mut seg.arguments { 159 | PathArguments::None | PathArguments::Parenthesized(_) => (), 160 | PathArguments::AngleBracketed(x) => { 161 | for arg in &mut x.args { 162 | if let GenericArgument::Lifetime(lt) = arg { 163 | lt.ident = Ident::new("static", Span::call_site()); 164 | } 165 | } 166 | } 167 | } 168 | } 169 | } 170 | Type::Reference(ty) => ty.lifetime = None, 171 | _ => (), 172 | } 173 | 174 | true 175 | } 176 | } 177 | 178 | Visitor.visit(unsafe_cell_mut(&mut self)); 179 | 180 | self 181 | } 182 | } 183 | 184 | trait TypeVisitor: Sized { 185 | fn visit_type(&mut self, ty: &UnsafeCell) -> bool { 186 | let _ty = ty; 187 | 188 | true 189 | } 190 | 191 | fn visit_lifetime(&mut self, lt: &UnsafeCell) -> bool { 192 | let _lt = lt; 193 | 194 | true 195 | } 196 | 197 | fn visit(&mut self, ty: &UnsafeCell) -> bool { 198 | fn visit_path(this: &mut X, path: &Path) -> bool { 199 | for seg in &path.segments { 200 | match &seg.arguments { 201 | PathArguments::None => (), 202 | PathArguments::AngleBracketed(x) => { 203 | for arg in &x.args { 204 | match arg { 205 | GenericArgument::Type(t) => { 206 | if !this.visit(unsafe_cell_ref(t)) { 207 | return false; 208 | } 209 | } 210 | GenericArgument::Lifetime(lt) => { 211 | if !this.visit_lifetime(unsafe_cell_ref(lt)) { 212 | return false; 213 | } 214 | } 215 | GenericArgument::AssocType(t) => { 216 | if !this.visit(unsafe_cell_ref(&t.ty)) { 217 | return false; 218 | } 219 | } 220 | _ => (), 221 | } 222 | } 223 | } 224 | PathArguments::Parenthesized(x) => { 225 | for t in &x.inputs { 226 | if !this.visit(unsafe_cell_ref(t)) { 227 | return false; 228 | } 229 | } 230 | 231 | match &x.output { 232 | ReturnType::Type(_, t) => { 233 | if !this.visit(unsafe_cell_ref(t)) { 234 | return false; 235 | } 236 | } 237 | ReturnType::Default => (), 238 | } 239 | } 240 | } 241 | } 242 | 243 | true 244 | } 245 | 246 | if !self.visit_type(ty) { 247 | return false; 248 | } 249 | 250 | let ty = unsafe { &*ty.get() }; 251 | 252 | match ty { 253 | Type::Path(ty) => visit_path(self, &ty.path), 254 | Type::Reference(t) => { 255 | if let Some(lt) = &t.lifetime { 256 | if !self.visit_lifetime(unsafe_cell_ref(lt)) { 257 | return false; 258 | } 259 | } 260 | 261 | if !self.visit(unsafe_cell_ref(&t.elem)) { 262 | return false; 263 | } 264 | 265 | true 266 | } 267 | Type::Array(t) => self.visit(unsafe_cell_ref(&t.elem)), 268 | Type::Slice(t) => self.visit(unsafe_cell_ref(&t.elem)), 269 | Type::Tuple(t) => { 270 | for t in &t.elems { 271 | if !self.visit(unsafe_cell_ref(t)) { 272 | return false; 273 | } 274 | } 275 | 276 | true 277 | } 278 | Type::TraitObject(t) => { 279 | for b in &t.bounds { 280 | match b { 281 | TypeParamBound::Lifetime(lt) => { 282 | if !self.visit_lifetime(unsafe_cell_ref(lt)) { 283 | return false; 284 | } 285 | } 286 | TypeParamBound::Trait(t) => { 287 | if !visit_path(self, &t.path) { 288 | return false; 289 | } 290 | } 291 | _ => (), 292 | } 293 | } 294 | 295 | true 296 | } 297 | _ => true, 298 | } 299 | } 300 | } 301 | 302 | fn unsafe_cell_ref(value: &T) -> &UnsafeCell { 303 | unsafe { &*(from_ref(value).cast::>()) } 304 | } 305 | 306 | fn unsafe_cell_mut(value: &mut T) -> &UnsafeCell { 307 | unsafe { &*(from_mut(value) as *const std::cell::UnsafeCell) } 308 | } 309 | -------------------------------------------------------------------------------- /murf-macros/src/mock/expectation.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Not; 2 | 3 | use proc_macro2::TokenStream; 4 | use quote::{quote, ToTokens}; 5 | use syn::ItemImpl; 6 | 7 | use crate::misc::{FormattedString, GenericsEx, IterEx, TempLifetimes}; 8 | 9 | use super::context::{ContextData, ImplContextData, MethodContext, MethodContextData}; 10 | 11 | pub(crate) struct Expectation { 12 | context: MethodContext, 13 | 14 | display: String, 15 | default_matcher: String, 16 | } 17 | 18 | impl Expectation { 19 | pub(crate) fn new(context: MethodContext, impl_: &ItemImpl) -> Self { 20 | let display = if let Some(trait_) = &context.trait_ { 21 | format!( 22 | "<{} as {}>::{}", 23 | context.impl_.self_ty.to_token_stream(), 24 | trait_.to_formatted_string(), 25 | context.ident_method, 26 | ) 27 | } else { 28 | format!( 29 | "{}::{}", 30 | impl_.self_ty.to_token_stream(), 31 | context.ident_method 32 | ) 33 | }; 34 | 35 | let default_matcher = format!( 36 | "({})", 37 | context 38 | .args_prepared 39 | .iter() 40 | .map(|_| "_") 41 | .collect::>() 42 | .join(", ") 43 | ); 44 | 45 | Expectation { 46 | context, 47 | 48 | display, 49 | default_matcher, 50 | } 51 | } 52 | } 53 | 54 | impl ToTokens for Expectation { 55 | #[allow(clippy::too_many_lines)] 56 | fn to_tokens(&self, tokens: &mut TokenStream) { 57 | let Self { 58 | context, 59 | 60 | display, 61 | default_matcher, 62 | } = self; 63 | 64 | let MethodContextData { 65 | context, 66 | is_associated, 67 | ga_expectation, 68 | lts_temp: TempLifetimes(lts_temp), 69 | lts_mock: TempLifetimes(lts_mock), 70 | 71 | args_prepared, 72 | args_prepared_lt, 73 | 74 | return_type, 75 | type_signature, 76 | .. 77 | } = &**context; 78 | 79 | let ImplContextData { context, .. } = &**context; 80 | 81 | let ContextData { 82 | ident_murf, 83 | trait_send, 84 | trait_sync, 85 | .. 86 | } = &**context; 87 | 88 | let trait_send = is_associated 89 | .then(|| quote!( + Send)) 90 | .or_else(|| trait_send.clone()); 91 | let trait_sync = is_associated 92 | .then(|| quote!( + Sync)) 93 | .or_else(|| trait_sync.clone()); 94 | 95 | let lts_temp = lts_temp.is_empty().not().then(|| quote!(< #lts_temp >)); 96 | let lts_mock = lts_mock.is_empty().not().then(|| quote!(for < #lts_mock >)); 97 | let lt = if *is_associated { 98 | quote!(+ 'static) 99 | } else { 100 | quote!(+ 'mock) 101 | }; 102 | 103 | let type_signature = type_signature.parenthesis(); 104 | let arg_types_prepared = args_prepared.iter().map(|t| &t.ty).parenthesis(); 105 | let arg_types_prepared_lt = args_prepared_lt.iter().map(|t| &t.ty).parenthesis(); 106 | 107 | let ga_expectation_phantom = ga_expectation.make_phantom_data(); 108 | let (ga_expectation_impl, ga_expectation_types, ga_expectation_where) = 109 | ga_expectation.split_for_impl(); 110 | 111 | tokens.extend(quote! { 112 | /// Defines the values of an expected call to a mocked method of the mock object. 113 | #[allow(clippy::type_complexity)] 114 | pub struct Expectation #ga_expectation_impl #ga_expectation_where { 115 | /// Defines how often the call is expected to be executed. 116 | pub times: Times, 117 | 118 | /// Human readable description of the expectation. 119 | pub description: Option, 120 | 121 | /// Action that is executed once the actual call to the mocked method is made. 122 | pub action: Option #trait_send #trait_sync #lt>>, 123 | 124 | /// Matcher that is used to verify the arguments of the call. 125 | pub matcher: Option #trait_send #trait_sync #lt>>, 126 | 127 | /// List of sequences the expectation must respect. 128 | pub sequences: Vec, 129 | 130 | _marker: #ga_expectation_phantom, 131 | } 132 | 133 | impl #ga_expectation_impl Default for Expectation #ga_expectation_types #ga_expectation_where { 134 | fn default() -> Self { 135 | Self { 136 | times: Times::default(), 137 | description: None, 138 | action: None, 139 | matcher: None, 140 | sequences: Vec::new(), 141 | _marker: PhantomData, 142 | } 143 | } 144 | } 145 | 146 | impl #ga_expectation_impl Debug for Expectation #ga_expectation_types #ga_expectation_where { 147 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 148 | f.debug_struct("Expectation") 149 | .field("times", &self.times) 150 | .field("description", &self.description) 151 | .field("action", &self.action.is_some()) 152 | .field("matcher", &self.matcher.is_some()) 153 | .field("sequences", &self.sequences) 154 | .finish() 155 | } 156 | } 157 | 158 | impl #ga_expectation_impl Expectation #ga_expectation_types #ga_expectation_where { 159 | /// Check if the arguments of a call matches the expectation. 160 | /// 161 | /// # Returns 162 | /// Returns `true` if the arguments are valid, `false` otherwise. 163 | pub fn matches #lts_temp (&self, args: &#arg_types_prepared) -> bool { 164 | if let Some(m) = &self.matcher { 165 | m.matches(args) 166 | } else { 167 | true 168 | } 169 | } 170 | } 171 | 172 | impl #ga_expectation_impl #ident_murf :: Expectation for Expectation #ga_expectation_types #ga_expectation_where { 173 | fn type_id(&self) -> usize { 174 | *TYPE_ID 175 | } 176 | 177 | fn is_ready(&self) -> bool { 178 | self.times.is_ready() 179 | } 180 | 181 | fn set_done(&self) { 182 | for seq_handle in &self.sequences { 183 | seq_handle.set_done(); 184 | } 185 | } 186 | 187 | fn type_signature(&self) -> &'static str { 188 | type_name::<#type_signature>() 189 | } 190 | } 191 | 192 | impl #ga_expectation_impl Display for Expectation #ga_expectation_types #ga_expectation_where { 193 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 194 | write!(f, #display)?; 195 | 196 | if let Some(m) = &self.matcher { 197 | write!(f, "(")?; 198 | m.fmt(f)?; 199 | write!(f, ")")?; 200 | } else { 201 | write!(f, #default_matcher)?; 202 | } 203 | 204 | if let Some(d) = &self.description { 205 | write!(f, " {d}")?; 206 | } 207 | 208 | Ok(()) 209 | } 210 | } 211 | }); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /murf-macros/src/mock/expectation_builder.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Not; 2 | 3 | use proc_macro2::TokenStream; 4 | use quote::{quote, ToTokens}; 5 | use syn::ImplItemFn; 6 | 7 | use crate::misc::{IterEx, MethodEx, TempLifetimes}; 8 | 9 | use super::{ 10 | context::{ContextData, MethodContext, MethodContextData}, 11 | parsed::Parsed, 12 | }; 13 | 14 | pub(crate) struct ExpectationBuilder { 15 | context: MethodContext, 16 | 17 | must_use: Option, 18 | } 19 | 20 | impl ExpectationBuilder { 21 | pub(crate) fn new(context: MethodContext, parsed: &Parsed, method: &ImplItemFn) -> Self { 22 | let must_use = (method.need_default_impl() && !method.has_default_impl() && !parsed.ty.is_extern()).then(|| quote!(#[must_use = "You need to define an action for this expectation because it has no default action!"])); 23 | 24 | ExpectationBuilder { context, must_use } 25 | } 26 | } 27 | 28 | impl ToTokens for ExpectationBuilder { 29 | #[allow(clippy::too_many_lines)] 30 | fn to_tokens(&self, tokens: &mut TokenStream) { 31 | let Self { context, must_use } = self; 32 | 33 | let MethodContextData { 34 | context, 35 | is_associated, 36 | ident_expectation_field, 37 | ga_expectation, 38 | ga_expectation_builder, 39 | lts_mock: TempLifetimes(lts_mock), 40 | args_prepared_lt, 41 | return_type, 42 | .. 43 | } = &**context; 44 | 45 | let ContextData { 46 | ident_murf, 47 | trait_send, 48 | trait_sync, 49 | ga_handle, 50 | .. 51 | } = &****context; 52 | 53 | let trait_send = is_associated 54 | .then(|| quote!( + Send)) 55 | .or_else(|| trait_send.clone()); 56 | let trait_sync = is_associated 57 | .then(|| quote!( + Sync)) 58 | .or_else(|| trait_sync.clone()); 59 | 60 | let lts_mock = lts_mock.is_empty().not().then(|| quote!(for < #lts_mock >)); 61 | let lt = if *is_associated { 62 | quote!( + 'static) 63 | } else { 64 | quote!( + 'mock) 65 | }; 66 | 67 | let arg_types_prepared_lt = args_prepared_lt.iter().map(|t| &t.ty).parenthesis(); 68 | 69 | let drop_handler = if *is_associated { 70 | quote! { 71 | let expectation: Box = Box::new(expectation); 72 | let expectation = Arc::new(Mutex::new(expectation)); 73 | let weak = Arc::downgrade(&expectation); 74 | 75 | if let Some(local) = #ident_murf :: LocalContext::current().borrow_mut().as_mut() { 76 | local.push(*TYPE_ID, weak) 77 | } else { 78 | EXPECTATIONS.lock().push(weak); 79 | }; 80 | } 81 | } else { 82 | quote! { 83 | let expectation = Box::new(expectation); 84 | } 85 | }; 86 | 87 | let (_ga_handle_impl, ga_handle_types, _ga_handle_where) = ga_handle.split_for_impl(); 88 | let (_ga_expectation_impl, ga_expectation_types, _ga_expectation_where) = 89 | ga_expectation.split_for_impl(); 90 | let ( 91 | ga_expectation_builder_impl, 92 | ga_expectation_builder_types, 93 | ga_expectation_builder_where, 94 | ) = ga_expectation_builder.split_for_impl(); 95 | 96 | tokens.extend(quote! { 97 | /// Helper type that is used to set the values of a [`Expectation`] object before it is 98 | /// added to the list of expected calls of a mock object. 99 | #must_use 100 | pub struct ExpectationBuilder #ga_expectation_builder_impl #ga_expectation_builder_where { 101 | handle: &'mock_exp Handle #ga_handle_types, 102 | expectation: Option, 103 | } 104 | 105 | impl #ga_expectation_builder_impl ExpectationBuilder #ga_expectation_builder_types #ga_expectation_builder_where { 106 | /// Create a new [`ExpectationBuilder`] object 107 | pub fn new(handle: &'mock_exp Handle #ga_handle_types,) -> Self { 108 | let mut expectation = Expectation { 109 | sequences: InSequence::create_handle().into_iter().collect(), 110 | ..Expectation::default() 111 | }; 112 | expectation.times.range = (1..).into(); 113 | 114 | Self { 115 | handle, 116 | expectation: Some(expectation), 117 | } 118 | } 119 | 120 | /// Add a description to the expectation. 121 | pub fn description>(mut self, value: S) -> Self { 122 | self.expectation().description = Some(value.into()); 123 | 124 | self 125 | } 126 | 127 | /// Add a [`Matcher`] to the expectation. 128 | /// 129 | /// Matchers can be used to verify that the arguments of method call matches the expectation. 130 | pub fn with #trait_send #trait_sync #lt>(mut self, matcher: M) -> Self { 131 | self.expectation().matcher = Some(Box::new(matcher)); 132 | 133 | self 134 | } 135 | 136 | /// Set the sequence the expectation should be executed in. 137 | pub fn in_sequence(mut self, sequence: &Sequence) -> Self { 138 | self.expectation().sequences = vec![ sequence.create_handle() ]; 139 | 140 | self 141 | } 142 | 143 | /// Add a sequence the expectation should be executed in. 144 | pub fn add_sequence(mut self, sequence: &Sequence) -> Self { 145 | self.expectation().sequences.push(sequence.create_handle()); 146 | 147 | self 148 | } 149 | 150 | /// Remove the expectation from all sequences. 151 | pub fn no_sequences(mut self) -> Self { 152 | self.expectation().sequences.clear(); 153 | 154 | self 155 | } 156 | 157 | /// Specify the number of calls for this expectation. 158 | pub fn times>(mut self, range: R) -> Self { 159 | self.expectation().times.range = range.into(); 160 | 161 | self 162 | } 163 | 164 | /// Specify an action that should be executed once the actual call to the linked method was made. 165 | /// 166 | /// This will set `.times(1)` before the action is added. I you want to use 167 | /// repeatedly executed actions please have a look at [`will_repeatedly`](Self::will_repeatedly). 168 | pub fn will_once(self, action: A) 169 | where 170 | A: #lts_mock Action<#arg_types_prepared_lt, #return_type> #trait_send #trait_sync #lt, 171 | { 172 | self.times(1).expectation().action = Some(Box::new(OnetimeAction::new(action))); 173 | } 174 | 175 | /// Specify an action that should be executed each time a call to the linked method was made. 176 | pub fn will_repeatedly(mut self, action: A) 177 | where 178 | A: #lts_mock Action<#arg_types_prepared_lt, #return_type> #trait_send #trait_sync + Clone #lt, 179 | { 180 | self.expectation().action = Some(Box::new(RepeatedAction::new(action))); 181 | } 182 | 183 | fn expectation(&mut self) -> &mut Expectation #ga_expectation_types { 184 | self.expectation.as_mut().unwrap() 185 | } 186 | } 187 | 188 | impl #ga_expectation_builder_impl Debug for ExpectationBuilder #ga_expectation_builder_types #ga_expectation_builder_where { 189 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 190 | f.debug_struct("ExpectationBuilder") 191 | .field("handle", &self.handle) 192 | .field("expectation", self.expectation.as_ref().unwrap()) 193 | .finish() 194 | } 195 | } 196 | 197 | impl #ga_expectation_builder_impl Drop for ExpectationBuilder #ga_expectation_builder_types #ga_expectation_builder_where { 198 | fn drop(&mut self) { 199 | if let Some(expectation) = self.expectation.take() { 200 | let desc = expectation.to_string(); 201 | for seq_handle in &expectation.sequences { 202 | seq_handle.set_description(desc.clone()); 203 | 204 | if expectation.times.is_ready() { 205 | for seq_handle in &expectation.sequences { 206 | seq_handle.set_ready(); 207 | } 208 | } 209 | } 210 | 211 | #drop_handler; 212 | 213 | self.handle.shared.lock().#ident_expectation_field.push(expectation); 214 | } 215 | } 216 | } 217 | }); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /murf-macros/src/mock/expectation_module.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{quote, ToTokens}; 3 | use syn::{ImplItemFn, ItemImpl}; 4 | 5 | use super::{ 6 | context::{MethodContext, MethodContextData}, 7 | expectation::Expectation, 8 | expectation_builder::ExpectationBuilder, 9 | parsed::Parsed, 10 | }; 11 | 12 | pub(crate) struct ExpectationModule { 13 | pub context: MethodContext, 14 | pub expectation: Expectation, 15 | pub expectation_builder: ExpectationBuilder, 16 | } 17 | 18 | impl ExpectationModule { 19 | pub(crate) fn new( 20 | context: MethodContext, 21 | parsed: &Parsed, 22 | impl_: &ItemImpl, 23 | method: &ImplItemFn, 24 | ) -> Self { 25 | let expectation = Expectation::new(context.clone(), impl_); 26 | let expectation_builder = ExpectationBuilder::new(context.clone(), parsed, method); 27 | 28 | Self { 29 | context, 30 | expectation, 31 | expectation_builder, 32 | } 33 | } 34 | } 35 | 36 | impl ToTokens for ExpectationModule { 37 | fn to_tokens(&self, tokens: &mut TokenStream) { 38 | let Self { 39 | context, 40 | expectation, 41 | expectation_builder, 42 | } = self; 43 | 44 | let MethodContextData { 45 | trait_, 46 | ident_method, 47 | is_associated, 48 | ident_expectation_module, 49 | .. 50 | } = &**context; 51 | 52 | let ident_murf = &context.ident_murf; 53 | 54 | let associated_expectations = if *is_associated { 55 | Some(quote! { 56 | pub static EXPECTATIONS: Lazy>>>>> = Lazy::new(|| Default::default()); 57 | 58 | pub fn cleanup_associated_expectations() { 59 | EXPECTATIONS.lock().retain_mut(|ex| ex.strong_count() > 0); 60 | } 61 | }) 62 | } else { 63 | None 64 | }; 65 | 66 | let type_ = if let Some(trait_) = trait_ { 67 | trait_.into_token_stream().to_string() 68 | } else { 69 | context.ident_state.to_string() 70 | }; 71 | 72 | let doc = format!("Module that defines the expectation types for [`{type_}::{ident_method}`]({type_}::{ident_method}"); 73 | 74 | tokens.extend(quote! { 75 | #[doc = #doc] 76 | pub mod #ident_expectation_module { 77 | use std::marker::PhantomData; 78 | use std::fmt::{Display, Formatter, Result as FmtResult}; 79 | 80 | use #ident_murf :: { 81 | Matcher, Times, TimesRange, Sequence, SequenceHandle, InSequence, 82 | action::{Action, RepeatableAction, OnetimeAction, RepeatedAction}, 83 | }; 84 | 85 | #[allow(clippy::wildcard_imports)] 86 | use super::*; 87 | 88 | #expectation 89 | #expectation_builder 90 | 91 | #associated_expectations 92 | 93 | /// Unique ID that represents this type of expectation. 94 | pub static TYPE_ID: Lazy = Lazy::new(#ident_murf :: next_type_id); 95 | } 96 | }); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /murf-macros/src/mock/handle.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{quote, ToTokens}; 3 | use syn::Generics; 4 | 5 | use crate::misc::GenericsEx; 6 | 7 | use super::context::{Context, ContextData, MethodContext, MethodContextData}; 8 | 9 | pub(crate) struct Handle { 10 | context: Context, 11 | methods: Vec, 12 | ga_handle_extra: Generics, 13 | } 14 | 15 | impl Handle { 16 | pub(crate) fn new(context: Context) -> Self { 17 | let ga_handle_extra = context.ga_handle.clone().add_lifetime_bounds("'mock"); 18 | 19 | Self { 20 | context, 21 | methods: Vec::new(), 22 | ga_handle_extra, 23 | } 24 | } 25 | 26 | pub(crate) fn add_method(&mut self, context: MethodContext) { 27 | self.methods.push(context); 28 | } 29 | 30 | fn render_method(context: &MethodContextData) -> TokenStream { 31 | let MethodContextData { 32 | trait_, 33 | is_associated, 34 | ident_method, 35 | ident_expect_method, 36 | ident_expectation_module, 37 | ga_method, 38 | ga_expectation, 39 | .. 40 | } = context; 41 | 42 | let mut ga_builder = ga_expectation.clone(); 43 | if *is_associated { 44 | ga_builder = ga_builder.add_lifetime("'mock"); 45 | } 46 | ga_builder = ga_builder.add_lifetime("'_"); 47 | 48 | let (ga_method_impl, _ga_method_types, ga_method_where) = ga_method.split_for_impl(); 49 | let (_ga_builder_impl, ga_builder_types, _ga_builder_where) = ga_builder.split_for_impl(); 50 | 51 | let type_ = if let Some(trait_) = trait_ { 52 | trait_.into_token_stream().to_string() 53 | } else { 54 | context.ident_state.to_string() 55 | }; 56 | 57 | let doc = format!( 58 | r"Add a new expectation for the [`{type_}::{ident_method}`]({type_}::{ident_method}) method to the mocked object. 59 | 60 | # Returns 61 | Returns an [`ExpectationBuilder`]({ident_expectation_module}::ExpectationBuilder) that 62 | can be used to specialize the expectation further." 63 | ); 64 | 65 | quote! { 66 | #[doc = #doc] 67 | pub fn #ident_expect_method #ga_method_impl(&self) -> #ident_expectation_module::ExpectationBuilder #ga_builder_types 68 | #ga_method_where 69 | { 70 | #ident_expectation_module::ExpectationBuilder::new(self) 71 | } 72 | } 73 | } 74 | } 75 | 76 | impl ToTokens for Handle { 77 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 78 | let Self { 79 | context, 80 | methods, 81 | ga_handle_extra, 82 | } = self; 83 | 84 | let ContextData { ga_handle, .. } = &**context; 85 | 86 | let (ga_handle_impl, ga_handle_types, ga_handle_where) = ga_handle.split_for_impl(); 87 | let (ga_handle_extra_impl, ga_handle_extra_types, ga_handle_extra_where) = 88 | ga_handle_extra.split_for_impl(); 89 | 90 | let methods = methods.iter().map(|context| Self::render_method(context)); 91 | 92 | tokens.extend(quote! { 93 | /// Handle that is used to control the mocked object. 94 | /// 95 | /// The handle can be used to add new expectations to a mocked object 96 | /// or to verify that all expectations has been fulfilled. 97 | /// 98 | /// If the handle is dropped, all expectations are verified automatically. 99 | /// I one of the expectations was not fulfilled a panic is raised. 100 | #[must_use] 101 | pub struct Handle #ga_handle_impl #ga_handle_where { 102 | /// Shared state that is used across the different helper objects of one mocked object. 103 | pub shared: Arc>, 104 | 105 | /// Whether to check if expectations are fulfilled on drop or not. 106 | pub check_on_drop: bool, 107 | } 108 | 109 | impl #ga_handle_impl Handle #ga_handle_types #ga_handle_where { 110 | /// Check if all expectations has been fulfilled. 111 | /// 112 | /// # Panics 113 | /// 114 | /// Panics if at least one expectation was nof fulfilled. 115 | pub fn checkpoint(&self) { 116 | if let Some(mut locked) = self.shared.try_lock() { 117 | locked.checkpoint(); 118 | } else { 119 | panic!("Unable to lock handle: Deadlock? Make sure that you do not drop a handle (or call `checkpoint` directly) of the same object inside an action of an expectation."); 120 | } 121 | } 122 | 123 | /// Returns a reference to itself. 124 | /// 125 | /// This is used to make the public API of the handle compatible to the mock object. 126 | pub fn mock_handle(&self) -> &Self { 127 | self 128 | } 129 | 130 | /// Drop the handle without checking the expectations. 131 | pub fn release(mut self) { 132 | self.check_on_drop = false; 133 | 134 | drop(self); 135 | } 136 | } 137 | 138 | impl #ga_handle_extra_impl Handle #ga_handle_extra_types #ga_handle_extra_where { 139 | #( #methods )* 140 | } 141 | 142 | impl #ga_handle_impl Handle #ga_handle_types #ga_handle_where { 143 | /// Create a new [`Handle`] object. 144 | pub fn new() -> Self { 145 | Self { 146 | shared: Arc::default(), 147 | check_on_drop: true, 148 | } 149 | } 150 | } 151 | 152 | impl #ga_handle_impl Clone for Handle #ga_handle_types #ga_handle_where { 153 | fn clone(&self) -> Self { 154 | Self { 155 | shared: self.shared.clone(), 156 | check_on_drop: self.check_on_drop, 157 | } 158 | } 159 | } 160 | 161 | impl #ga_handle_impl Default for Handle #ga_handle_types #ga_handle_where { 162 | fn default() -> Self { 163 | Self::new() 164 | } 165 | } 166 | 167 | impl #ga_handle_impl Debug for Handle #ga_handle_types #ga_handle_where { 168 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 169 | f.debug_struct("Handle") 170 | .field("shared", &self.shared) 171 | .field("check_on_drop", &self.check_on_drop) 172 | .finish() 173 | } 174 | } 175 | 176 | impl #ga_handle_impl Drop for Handle #ga_handle_types #ga_handle_where { 177 | fn drop(&mut self) { 178 | if self.check_on_drop && !::std::thread::panicking() { 179 | self.checkpoint(); 180 | } 181 | } 182 | } 183 | }); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /murf-macros/src/mock/mock.rs: -------------------------------------------------------------------------------- 1 | use quote::{quote, ToTokens}; 2 | use syn::ImplItem; 3 | 4 | use crate::mock::context::ImplContextData; 5 | 6 | use super::context::{Context, ContextData, ImplContext}; 7 | 8 | pub(crate) struct Mock { 9 | context: Context, 10 | impls: Vec, 11 | } 12 | 13 | struct Impl { 14 | context: ImplContext, 15 | items: Vec, 16 | } 17 | 18 | impl Mock { 19 | pub(crate) fn new(context: Context) -> Self { 20 | Self { 21 | context, 22 | impls: Vec::new(), 23 | } 24 | } 25 | 26 | pub(crate) fn add_impl(&mut self, context: ImplContext, items: Vec) { 27 | self.impls.push(Impl { context, items }); 28 | } 29 | } 30 | 31 | impl ToTokens for Mock { 32 | #[allow(clippy::too_many_lines)] 33 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 34 | let Self { context, impls } = self; 35 | 36 | let ContextData { 37 | ident_state, 38 | ga_state, 39 | ga_mock, 40 | ga_handle, 41 | derive_clone, 42 | derive_default, 43 | .. 44 | } = &**context; 45 | 46 | let (ga_mock_impl, ga_mock_types, ga_mock_where) = ga_mock.split_for_impl(); 47 | let (_ga_state_impl, ga_state_types, _ga_state_where) = ga_state.split_for_impl(); 48 | let (_ga_handle_impl, ga_handle_types, _ga_handle_where) = ga_handle.split_for_impl(); 49 | 50 | let mock_clone_impl = derive_clone.then(|| { 51 | quote! { 52 | impl #ga_mock_impl Clone for Mock #ga_mock_types #ga_mock_where { 53 | fn clone(&self) -> Self { 54 | Self { 55 | state: self.state.clone(), 56 | shared: self.shared.clone(), 57 | handle: self.handle.clone(), 58 | } 59 | } 60 | } 61 | } 62 | }); 63 | 64 | let mock_default_impl = derive_default.then(|| { 65 | quote! { 66 | impl #ga_mock_impl Mock #ga_mock_types #ga_mock_where { 67 | /// Create a new empty mock object. 68 | /// 69 | /// This method is only generated if the object to mock implements 70 | /// the `Default` trait. 71 | pub fn new() -> Self { 72 | #[allow(clippy::default_trait_access)] 73 | Self::from_state(Default::default()) 74 | } 75 | } 76 | 77 | impl #ga_mock_impl Default for Mock #ga_mock_types #ga_mock_where { 78 | fn default() -> Self { 79 | Self::new() 80 | } 81 | } 82 | } 83 | }); 84 | 85 | let impls = impls.iter().map(|impl_| { 86 | let Impl { context, items } = impl_; 87 | let ImplContextData { 88 | trait_, 89 | ga_impl_mock, 90 | .. 91 | } = &**context; 92 | 93 | let trait_ = trait_.as_ref().map(|t| quote!( #t for )); 94 | 95 | let (ga_impl, _ga_types, ga_where) = ga_impl_mock.split_for_impl(); 96 | 97 | quote! { 98 | impl #ga_impl #trait_ Mock #ga_mock_types #ga_where { 99 | #( #items )* 100 | } 101 | } 102 | }); 103 | 104 | tokens.extend(quote! { 105 | /// Mocked version of the type the [`mock!`](crate::mock) macro was executed on. 106 | #[must_use] 107 | pub struct Mock #ga_mock_impl #ga_mock_where { 108 | /// The state is the object that the [`mock!`](crate::mock) macro was executed on. 109 | /// It is used to execute actual calls to the mocked version of the different methods 110 | /// of the object. 111 | pub state: #ident_state #ga_state_types, 112 | 113 | /// Shared state that is used across the different helper objects of one mocked object. 114 | pub (super) shared: Arc>, 115 | 116 | /// Handle of the current mock object. 117 | pub (super) handle: Option 118 | } 119 | 120 | impl #ga_mock_impl Mock #ga_mock_types #ga_mock_where { 121 | /// Create a new [`Mock`] instance from the passed `state` object. 122 | pub fn from_state(state: #ident_state #ga_state_types) -> Self { 123 | let handle = Handle::new(); 124 | let shared = handle.shared.clone(); 125 | 126 | Self { 127 | state, 128 | shared, 129 | handle: Some(handle), 130 | } 131 | } 132 | 133 | /// Get a reference to the handle of this mock object. 134 | /// 135 | /// # Panics 136 | /// May panic if the current mock object does not contain a handle anymore. 137 | pub fn mock_handle(&self) -> &Handle #ga_handle_types { 138 | if let Some(handle) = &self.handle { 139 | handle 140 | } else { 141 | panic!("The handle of this mock object was already taken!"); 142 | } 143 | } 144 | 145 | /// Split the current mock object into its handle and a mock object 146 | /// without a handle. 147 | /// 148 | /// # Panics 149 | /// May panic if the current mock object does not contain a handle anymore. 150 | pub fn mock_split(mut self) -> (Handle #ga_handle_types, Self) { 151 | let handle = self.mock_take_handle(); 152 | 153 | (handle, self) 154 | } 155 | 156 | /// Extract the handle from the current mock object. 157 | /// 158 | /// # Panics 159 | /// May panic if the current mock object does not contain a handle anymore. 160 | pub fn mock_take_handle(&mut self) -> Handle #ga_handle_types { 161 | if let Some(handle) = self.handle.take() { 162 | handle 163 | } else { 164 | panic!("The handle of this mock object was already taken!"); 165 | } 166 | } 167 | 168 | /// Release the handle of this mock object. See [`release`](Handle::release()) for details. 169 | /// 170 | /// # Panics 171 | /// May panic if the current mock object does not contain a handle anymore. 172 | pub fn mock_release_handle(&mut self) { 173 | self.mock_take_handle().release(); 174 | } 175 | } 176 | 177 | impl #ga_mock_impl Debug for Mock #ga_mock_types #ga_mock_where { 178 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 179 | f.debug_struct("Mock") 180 | .field("shared", &self.shared) 181 | .field("handle", &self.handle) 182 | .finish_non_exhaustive() 183 | } 184 | } 185 | 186 | #mock_clone_impl 187 | #mock_default_impl 188 | 189 | #( #impls )* 190 | }); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /murf-macros/src/mock/mock_method.rs: -------------------------------------------------------------------------------- 1 | use quote::{quote, ToTokens}; 2 | use syn::{FnArg, ImplItemFn, Item, PatType, ReturnType, Stmt, Type}; 3 | 4 | use crate::misc::{AttribsEx, FormattedString, IterEx, TypeEx}; 5 | 6 | use super::context::{MethodContext, MethodContextData}; 7 | 8 | pub(crate) struct MockMethod; 9 | 10 | impl MockMethod { 11 | #[allow(clippy::too_many_lines)] 12 | pub(crate) fn render(context: &MethodContext, mut method: ImplItemFn) -> ImplItemFn { 13 | let MethodContextData { 14 | is_associated, 15 | no_default_impl, 16 | impl_, 17 | trait_, 18 | ga_expectation, 19 | ident_expectation_module, 20 | ident_expectation_field, 21 | args, 22 | ret, 23 | args_prepared, 24 | type_signature, 25 | .. 26 | } = &**context; 27 | 28 | let ident_murf = &context.ident_murf; 29 | 30 | let locked = if *is_associated { 31 | quote! { 32 | let locked = #ident_expectation_module::EXPECTATIONS.lock(); 33 | } 34 | } else { 35 | quote! { 36 | let shared = self.shared.clone(); 37 | let mut locked = shared.lock(); 38 | } 39 | }; 40 | 41 | let expectations_iter = if *is_associated { 42 | quote! { 43 | #ident_murf :: LocalContext::current() 44 | .borrow() 45 | .as_ref() 46 | .into_iter() 47 | .flat_map(|x| x.expectations(*#ident_expectation_module::TYPE_ID)) 48 | .chain(&*locked) 49 | } 50 | } else { 51 | quote!(&mut locked.#ident_expectation_field) 52 | }; 53 | 54 | let expectation_unwrap = is_associated.then(|| { 55 | quote! { 56 | let ex = if let Some(ex) = ex.upgrade() { 57 | ex 58 | } else { 59 | continue; 60 | }; 61 | let mut ex = ex.lock(); 62 | } 63 | }); 64 | 65 | let (_ga_expectation_impl, ga_expectation_types, _ga_expectation_where) = 66 | ga_expectation.split_for_impl(); 67 | 68 | let arg_names = args 69 | .iter() 70 | .map(|arg| match arg { 71 | FnArg::Receiver(_) => quote!(self), 72 | FnArg::Typed(PatType { pat, .. }) => quote!( #pat ), 73 | }) 74 | .parenthesis(); 75 | 76 | let type_signature = type_signature.parenthesis(); 77 | let arg_names_prepared = args_prepared.iter().map(|arg| &arg.pat).parenthesis(); 78 | 79 | let default_args = method.sig.inputs.iter().map(|i| match i { 80 | FnArg::Receiver(r) if r.ty.to_formatted_string() == "Pin<&mut Self>" => { 81 | quote!(unsafe { std::pin::Pin::new_unchecked(&mut this.get_unchecked_mut().state) }) 82 | } 83 | FnArg::Receiver(r) if r.ty.to_formatted_string() == "Arc" => { 84 | quote!(Arc::new(this.state.clone())) 85 | } 86 | FnArg::Receiver(r) if r.ty.to_formatted_string() == "&Arc" => { 87 | quote!(&Arc::new(this.state.clone())) 88 | } 89 | FnArg::Receiver(r) if r.reference.is_some() && r.mutability.is_some() => { 90 | quote!(&mut this.state) 91 | } 92 | FnArg::Receiver(r) if r.reference.is_some() => quote!(&this.state), 93 | FnArg::Receiver(_) => quote!(this.state), 94 | FnArg::Typed(t) => t.pat.to_token_stream(), 95 | }); 96 | 97 | let default_action = if *no_default_impl { 98 | quote!(panic!("No default implementation for expectation {}", ex)) 99 | } else if let Some(t) = trait_ { 100 | let ident = &method.sig.ident; 101 | let self_ty = &impl_.self_ty; 102 | let (_, ga_types, _) = method.sig.generics.split_for_impl(); 103 | let turbofish = ga_types.as_turbofish(); 104 | 105 | quote! { 106 | let #arg_names_prepared = args; 107 | #[allow(clippy::let_and_return)] 108 | let ret = <#self_ty as #t>::#ident #turbofish ( #( #default_args ),* ); 109 | } 110 | } else { 111 | let t = &impl_.self_ty; 112 | let ident = &method.sig.ident; 113 | let (_, ga_types, _) = method.sig.generics.split_for_impl(); 114 | let turbofish = ga_types.as_turbofish(); 115 | 116 | quote! { 117 | let #arg_names_prepared = args; 118 | #[allow(clippy::let_and_return)] 119 | let ret = #t::#ident #turbofish ( #( #default_args ),* ); 120 | } 121 | }; 122 | 123 | let result = match ret { 124 | _ if *no_default_impl => None, 125 | ReturnType::Default => Some(quote!(ret)), 126 | ReturnType::Type(_, t) => Some(match &**t { 127 | Type::Reference(r) 128 | if r.mutability.is_some() && r.elem.to_formatted_string() == "Self" => 129 | { 130 | quote!(&mut this) 131 | } 132 | Type::Reference(r) if r.elem.to_formatted_string() == "Self" => quote!(&this), 133 | t if t.to_formatted_string() == "Self" => quote!(Self { 134 | state: ret, 135 | shared: this.shared.clone(), 136 | handle: this.handle.clone(), 137 | }), 138 | t if t.to_formatted_string() == "Box" => quote!(Box), 143 | t if t.contains_self_type() => { 144 | let s = format!( 145 | "No default implementation for `{}` for expectation {{}}", 146 | t.to_formatted_string() 147 | ); 148 | 149 | quote!(panic!(#s, ex)) 150 | } 151 | _ => quote!(ret), 152 | }), 153 | }; 154 | 155 | let error = if let Some(t) = trait_ { 156 | format!( 157 | "<{} as {}>", 158 | impl_.self_ty.to_token_stream(), 159 | t.to_formatted_string() 160 | ) 161 | } else { 162 | format!("{}", impl_.self_ty.to_token_stream()) 163 | }; 164 | let error = format!( 165 | "No suitable expectation found for {}::{}", 166 | error, &method.sig.ident 167 | ); 168 | 169 | method.block.stmts = vec![Stmt::Item(Item::Verbatim(quote! { 170 | #locked 171 | let args = #arg_names; 172 | 173 | let mut msg = String::new(); 174 | let _ = writeln!(msg, #error); 175 | let _ = writeln!(msg, "Tried the following expectations:"); 176 | 177 | for ex in #expectations_iter { 178 | #expectation_unwrap 179 | 180 | let _ = writeln!(msg, "- {ex}"); 181 | 182 | /* type matches? */ 183 | if ex.type_signature() != type_name::<#type_signature>() { 184 | let _ = writeln!(msg, " Type signature: not ok"); 185 | let _ = writeln!(msg, " Expected: `{}`", type_name::<#type_signature>()); 186 | let _ = writeln!(msg, " But found: `{}`", ex.type_signature()); 187 | 188 | continue; 189 | } 190 | let _ = writeln!(msg, " Type signature: ok"); 191 | 192 | let ex: &mut dyn #ident_murf :: Expectation = &mut **ex; 193 | #[allow(clippy::cast_ptr_alignment)] 194 | let ex = unsafe { &mut *(std::ptr::from_mut::(ex).cast::<#ident_expectation_module::Expectation #ga_expectation_types>()) }; 195 | 196 | let mut is_valid = true; 197 | 198 | /* value matches? */ 199 | if ex.matches(&args) { 200 | let _ = writeln!(msg, " Argument matcher: ok"); 201 | } else { 202 | let _ = writeln!(msg, " Argument matcher: not ok"); 203 | 204 | is_valid = false; 205 | } 206 | 207 | /* is done? */ 208 | if ex.times.is_done() { 209 | let _ = writeln!(msg, " Call count: done"); 210 | 211 | is_valid = false; 212 | } else if ex.times.is_ready() { 213 | let _ = writeln!(msg, " Call count: ready"); 214 | } else { 215 | let _ = writeln!(msg, " Call count: ok"); 216 | } 217 | 218 | /* is active? */ 219 | for seq_handle in &ex.sequences { 220 | if seq_handle.is_done() { 221 | is_valid = false; 222 | 223 | let s = seq_handle.sequence_id().to_string(); 224 | let _ = writeln!(msg, " Sequence #{}:{:>2$}done", s, "", 10 - s.len()); 225 | } else if seq_handle.is_active() { 226 | let s = seq_handle.sequence_id().to_string(); 227 | let _ = writeln!(msg, " Sequence #{}:{:>2$}active", s, "", 10 - s.len()); 228 | } else { 229 | is_valid = false; 230 | 231 | let s = seq_handle.sequence_id().to_string(); 232 | let _ = writeln!(msg, " Sequence #{}:{:>2$}not active", s, "", 10 - s.len()); 233 | let _ = writeln!(msg, " has unsatisfied expectations"); 234 | 235 | for ex in seq_handle.unsatisfied() { 236 | let _ = writeln!(msg, " - {ex}"); 237 | } 238 | } 239 | } 240 | 241 | if !is_valid { 242 | continue; 243 | } 244 | 245 | /* execute */ 246 | ex.times.increment(); 247 | if ex.times.is_ready() { 248 | for seq_handle in &ex.sequences { 249 | seq_handle.set_ready(); 250 | } 251 | } 252 | 253 | return if let Some(action) = &mut ex.action { 254 | action.exec(args) 255 | } else { 256 | #default_action 257 | #result 258 | }; 259 | } 260 | 261 | println!("{msg}"); 262 | 263 | panic!(#error); 264 | }))]; 265 | 266 | method.remove_murf_attrs() 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /murf-macros/src/mock/mock_module.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{quote, ToTokens}; 3 | use syn::{ImplItem, ImplItemFn, ItemImpl}; 4 | 5 | use super::{ 6 | context::{Context, ContextData, ImplContext, MethodContext}, 7 | expectation_module::ExpectationModule, 8 | handle::Handle, 9 | mock::Mock, 10 | mock_method::MockMethod, 11 | mockable::Mockable, 12 | mockable_default::MockableDefault, 13 | parsed::Parsed, 14 | shared::Shared, 15 | }; 16 | 17 | /* MockModule */ 18 | 19 | pub(crate) struct MockModule { 20 | pub context: Context, 21 | pub mock: Mock, 22 | pub mockable: Mockable, 23 | pub mockable_default: MockableDefault, 24 | pub handle: Handle, 25 | pub shared: Shared, 26 | pub expectations: Vec, 27 | } 28 | 29 | impl MockModule { 30 | pub(crate) fn new(parsed: &Parsed) -> Self { 31 | let context = Context::new(parsed); 32 | let mock = Mock::new(context.clone()); 33 | let mockable = Mockable::new(context.clone()); 34 | let mockable_default = MockableDefault::new(context.clone()); 35 | let handle = Handle::new(context.clone()); 36 | let shared = Shared::new(context.clone()); 37 | let expectations = Vec::new(); 38 | 39 | let mut ret = Self { 40 | context, 41 | mock, 42 | mockable, 43 | mockable_default, 44 | handle, 45 | shared, 46 | expectations, 47 | }; 48 | 49 | ret.generate(parsed); 50 | 51 | ret 52 | } 53 | 54 | fn generate(&mut self, parsed: &Parsed) { 55 | for impl_ in &parsed.impls { 56 | let context = ImplContext::new(self.context.clone(), impl_); 57 | 58 | self.generate_impl(context, parsed, impl_); 59 | } 60 | } 61 | 62 | fn generate_impl(&mut self, context: ImplContext, parsed: &Parsed, impl_: &ItemImpl) { 63 | let items = impl_ 64 | .items 65 | .iter() 66 | .map(|item| self.generate_item(context.clone(), parsed, impl_, item)) 67 | .collect(); 68 | 69 | self.mock.add_impl(context, items); 70 | } 71 | 72 | fn generate_item( 73 | &mut self, 74 | context: ImplContext, 75 | parsed: &Parsed, 76 | impl_: &ItemImpl, 77 | item: &ImplItem, 78 | ) -> ImplItem { 79 | match item { 80 | ImplItem::Fn(f) => ImplItem::Fn(self.generate_method(context, parsed, impl_, f)), 81 | item => item.clone(), 82 | } 83 | } 84 | 85 | fn generate_method( 86 | &mut self, 87 | context: ImplContext, 88 | parsed: &Parsed, 89 | impl_: &ItemImpl, 90 | method: &ImplItemFn, 91 | ) -> ImplItemFn { 92 | let context = MethodContext::new(context, impl_, method); 93 | 94 | let ret = MockMethod::render(&context, method.clone()); 95 | 96 | self.handle.add_method(context.clone()); 97 | self.shared.add_expectation(context.clone()); 98 | self.expectations 99 | .push(ExpectationModule::new(context, parsed, impl_, method)); 100 | 101 | ret 102 | } 103 | } 104 | 105 | impl ToTokens for MockModule { 106 | fn to_tokens(&self, tokens: &mut TokenStream) { 107 | let Self { 108 | context, 109 | mock, 110 | mockable, 111 | mockable_default, 112 | handle, 113 | shared, 114 | expectations, 115 | } = self; 116 | 117 | let ContextData { 118 | ident_murf, 119 | ident_module, 120 | ident_mock, 121 | ident_handle, 122 | .. 123 | } = &**context; 124 | 125 | tokens.extend(quote! { 126 | pub use #ident_module::Mock as #ident_mock; 127 | pub use #ident_module::Handle as #ident_handle; 128 | pub use #ident_module::{Mockable as _, MockableDefault as _}; 129 | 130 | /// Implements the different mocked types for the type the [`mock!`](crate::mock) 131 | /// macro was executed on. 132 | pub mod #ident_module { 133 | use std::any::type_name; 134 | use std::fmt::{Write, Debug, Formatter, Result as FmtResult}; 135 | use std::marker::PhantomData; 136 | use std::mem::take; 137 | use std::sync::{Arc, Weak}; 138 | 139 | use parking_lot::Mutex; 140 | use #ident_murf :: {Lazy, Expectation}; 141 | 142 | #[allow(clippy::wildcard_imports)] 143 | use super::*; 144 | 145 | #mock 146 | #mockable 147 | #mockable_default 148 | #handle 149 | #shared 150 | 151 | #( #expectations )* 152 | } 153 | }); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /murf-macros/src/mock/mockable.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{quote, ToTokens}; 3 | 4 | use super::context::{Context, ContextData}; 5 | 6 | pub(crate) struct Mockable { 7 | context: Context, 8 | } 9 | 10 | impl Mockable { 11 | pub(crate) fn new(context: Context) -> Self { 12 | Self { context } 13 | } 14 | } 15 | 16 | impl ToTokens for Mockable { 17 | fn to_tokens(&self, tokens: &mut TokenStream) { 18 | let Self { context } = self; 19 | 20 | let ContextData { 21 | ident_state, 22 | ident_module, 23 | ga_mock, 24 | ga_state, 25 | ga_handle, 26 | extern_mock_lifetime, 27 | .. 28 | } = &**context; 29 | 30 | let (_ga_mock_impl, ga_mock_types, _ga_mock_where) = ga_mock.split_for_impl(); 31 | let (ga_state_impl, ga_state_types, ga_state_where) = ga_state.split_for_impl(); 32 | let (_ga_handle_impl, ga_handle_types, _ga_handle_where) = ga_handle.split_for_impl(); 33 | 34 | if *extern_mock_lifetime { 35 | tokens.extend(quote! { 36 | /// Helper trait that is used to convert a object into it's mocked version. 37 | pub trait Mockable<'mock> { 38 | /// Mocked version of the object. 39 | type Mock; 40 | 41 | /// Handle to control the mock object. 42 | type Handle; 43 | 44 | /// Returns a mocked version of the object. 45 | fn into_mock(self) -> Self::Mock; 46 | 47 | /// Returns a handle and a mocked version of the object. 48 | fn into_mock_with_handle(self) -> (Self::Handle, Self::Mock); 49 | } 50 | 51 | impl #ga_state_impl Mockable<'mock> for #ident_state #ga_state_types #ga_state_where { 52 | type Mock = #ident_module::Mock #ga_mock_types; 53 | type Handle = #ident_module::Handle #ga_handle_types; 54 | 55 | fn into_mock(self) -> Self::Mock { 56 | Self::Mock::from_state(self) 57 | } 58 | 59 | fn into_mock_with_handle(self) -> (Self::Handle, Self::Mock) { 60 | self.into_mock().mock_split() 61 | } 62 | } 63 | }); 64 | } else { 65 | tokens.extend(quote! { 66 | /// Helper trait that is used to convert a object into it's mocked version. 67 | pub trait Mockable { 68 | /// Mocked version of the object. 69 | type Mock<'mock>; 70 | 71 | /// Handle to control the mock object. 72 | type Handle<'mock>; 73 | 74 | /// Returns a mocked version of the object. 75 | fn into_mock<'mock>(self) -> Self::Mock<'mock>; 76 | 77 | /// Returns a handle and a mocked version of the object. 78 | fn into_mock_with_handle<'mock>(self) -> (Self::Handle<'mock>, Self::Mock<'mock>); 79 | } 80 | 81 | impl #ga_state_impl Mockable for #ident_state #ga_state_types #ga_state_where { 82 | type Mock<'mock> = #ident_module::Mock #ga_mock_types; 83 | type Handle<'mock> = #ident_module::Handle #ga_handle_types; 84 | 85 | fn into_mock<'mock>(self) -> Self::Mock<'mock> { 86 | Self::Mock::from_state(self) 87 | } 88 | 89 | fn into_mock_with_handle<'mock>(self) -> (Self::Handle<'mock>, Self::Mock<'mock>) { 90 | self.into_mock().mock_split() 91 | } 92 | } 93 | }); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /murf-macros/src/mock/mockable_default.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{quote, ToTokens}; 3 | 4 | use super::context::{Context, ContextData}; 5 | 6 | pub(crate) struct MockableDefault { 7 | pub context: Context, 8 | } 9 | 10 | impl MockableDefault { 11 | pub(crate) fn new(context: Context) -> Self { 12 | Self { context } 13 | } 14 | } 15 | 16 | impl ToTokens for MockableDefault { 17 | fn to_tokens(&self, tokens: &mut TokenStream) { 18 | let Self { context } = self; 19 | 20 | let ContextData { 21 | extern_mock_lifetime, 22 | .. 23 | } = &**context; 24 | 25 | if *extern_mock_lifetime { 26 | tokens.extend(quote! { 27 | /// Helper trait that is used to generate a default mock object 28 | /// from it's mocked type. 29 | pub trait MockableDefault<'mock>: Mockable<'mock> { 30 | /// Returns a new empty mock object. 31 | fn mock() -> Self::Mock; 32 | 33 | /// Returns a new empty handle and a mocked object. 34 | fn mock_with_handle() -> (Self::Handle, Self::Mock); 35 | } 36 | 37 | impl<'mock, X> MockableDefault<'mock> for X 38 | where 39 | X: Mockable<'mock> + Default, 40 | { 41 | fn mock() -> Self::Mock { 42 | Self::default().into_mock() 43 | } 44 | 45 | fn mock_with_handle() -> (Self::Handle, Self::Mock) { 46 | Self::default().into_mock_with_handle() 47 | } 48 | } 49 | }); 50 | } else { 51 | tokens.extend(quote! { 52 | /// Helper trait that is used to generate a default mock object 53 | /// from it's mocked type. 54 | pub trait MockableDefault: Mockable { 55 | /// Returns a new empty mock object. 56 | fn mock<'mock>() -> Self::Mock<'mock>; 57 | 58 | /// Returns a new empty handle and a mocked object. 59 | fn mock_with_handle<'mock>() -> (Self::Handle<'mock>, Self::Mock<'mock>); 60 | } 61 | 62 | impl MockableDefault for X 63 | where 64 | X: Mockable + Default, 65 | { 66 | fn mock<'mock>() -> Self::Mock<'mock> { 67 | Self::default().into_mock() 68 | } 69 | 70 | fn mock_with_handle<'mock>() -> (Self::Handle<'mock>, Self::Mock<'mock>) { 71 | Self::default().into_mock_with_handle() 72 | } 73 | } 74 | }); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /murf-macros/src/mock/mocked.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{quote, ToTokens}; 3 | 4 | use super::{mock_module::MockModule, parsed::Parsed}; 5 | 6 | /// Mocked implementation of a mock! macro 7 | pub(crate) struct Mocked { 8 | parsed: Parsed, 9 | mock_module: MockModule, 10 | } 11 | 12 | impl Mocked { 13 | pub(crate) fn new(parsed: Parsed) -> Self { 14 | let mock_module = MockModule::new(&parsed); 15 | 16 | Self { 17 | parsed, 18 | mock_module, 19 | } 20 | } 21 | } 22 | 23 | impl ToTokens for Mocked { 24 | fn to_tokens(&self, tokens: &mut TokenStream) { 25 | let Self { 26 | parsed, 27 | mock_module, 28 | } = self; 29 | 30 | tokens.extend(quote! { 31 | #parsed 32 | #mock_module 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /murf-macros/src/mock/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::module_inception)] 2 | mod context; 3 | mod expectation; 4 | mod expectation_builder; 5 | mod expectation_module; 6 | mod handle; 7 | mod mock; 8 | mod mock_method; 9 | mod mock_module; 10 | mod mockable; 11 | mod mockable_default; 12 | mod mocked; 13 | mod parsed; 14 | mod shared; 15 | 16 | use proc_macro2::TokenStream; 17 | use quote::ToTokens; 18 | use syn::parse2; 19 | 20 | use mocked::Mocked; 21 | use parsed::Parsed; 22 | 23 | pub(crate) fn exec(input: TokenStream) -> TokenStream { 24 | let mock = match parse2::(input) { 25 | Ok(parsed) => parsed, 26 | Err(err) => return err.to_compile_error(), 27 | }; 28 | 29 | #[cfg(feature = "debug-to-file")] 30 | let ident = mock.ty.ident().to_string(); 31 | 32 | #[allow(clippy::let_and_return)] 33 | let tokens = Mocked::new(mock).into_token_stream(); 34 | 35 | #[cfg(feature = "debug")] 36 | println!("\nmock!:\n{tokens:#}\n"); 37 | 38 | #[cfg(feature = "debug-to-file")] 39 | let _ = debug_to_file(&tokens, &ident); 40 | 41 | tokens 42 | } 43 | 44 | #[cfg(feature = "debug-to-file")] 45 | fn debug_to_file(tokens: &TokenStream, ident: &str) -> std::io::Result<()> { 46 | use std::fs::{create_dir_all, write}; 47 | use std::path::PathBuf; 48 | 49 | use convert_case::{Case, Casing}; 50 | use proc_macro::Span; 51 | 52 | let path = Span::call_site() 53 | .source_file() 54 | .path() 55 | .join(ident.to_case(Case::Snake)); 56 | let path = PathBuf::from("./target/generated").join(path); 57 | 58 | if let Some(parent) = path.parent() { 59 | create_dir_all(parent)?; 60 | } 61 | 62 | write(path, tokens.to_string())?; 63 | 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /murf-macros/src/mock/parsed.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, TokenStream}; 2 | use quote::{quote, ToTokens}; 3 | use syn::{ 4 | parse::{discouraged::Speculative, Parse, ParseStream, Parser, Result as ParseResult}, 5 | parse2, 6 | punctuated::Punctuated, 7 | token::{Brace, Comma}, 8 | Attribute, Block, Generics, ImplItem, ImplItemFn, Item, ItemEnum, ItemImpl, ItemStruct, Meta, 9 | Path, ReturnType, Stmt, TraitItemFn, Type, Visibility, 10 | }; 11 | 12 | use crate::misc::AttribsEx; 13 | 14 | /// Parsed code inside the mock! macro 15 | pub(crate) struct Parsed { 16 | pub ty: TypeToMock, 17 | pub impls: Vec, 18 | 19 | pub derive_send: bool, 20 | pub derive_sync: bool, 21 | } 22 | 23 | impl Parsed { 24 | fn add_default_impl(impl_: &mut ItemImpl) -> ParseResult<()> { 25 | for i in &mut impl_.items { 26 | if let ImplItem::Verbatim(ts) = i { 27 | let TraitItemFn { mut attrs, sig, .. } = parse2::(ts.clone())?; 28 | 29 | let mut block = Block { 30 | brace_token: Brace::default(), 31 | stmts: Vec::new(), 32 | }; 33 | 34 | if sig.output != ReturnType::Default { 35 | block.stmts = vec![Stmt::Item(Item::Verbatim(quote!( 36 | panic!("No default action specified!"); 37 | )))]; 38 | } 39 | 40 | let attr = quote!(#[allow(unused_variables)]); 41 | attrs.extend(Parser::parse2(Attribute::parse_outer, attr).unwrap()); 42 | 43 | *i = ImplItem::Fn(ImplItemFn { 44 | attrs, 45 | vis: Visibility::Inherited, 46 | defaultness: None, 47 | sig, 48 | block, 49 | }); 50 | } 51 | } 52 | 53 | Ok(()) 54 | } 55 | 56 | fn remove_uneeded_derives(ty: &mut TypeToMock) -> ParseResult<()> { 57 | let attrs = match ty { 58 | TypeToMock::Enum(o) => Some(&mut o.attrs), 59 | TypeToMock::Struct(o) => Some(&mut o.attrs), 60 | _ => None, 61 | }; 62 | 63 | if let Some(attrs) = attrs { 64 | for attr in attrs { 65 | if attr.path().is_ident("derive") { 66 | if let Meta::List(ml) = &mut attr.meta { 67 | let mut ret = Option::>::None; 68 | 69 | ml.parse_args_with(|p: ParseStream<'_>| { 70 | let ml = Punctuated::::parse_separated_nonempty(p)?; 71 | 72 | ret = Some( 73 | ml.into_iter() 74 | .filter(|p| !p.is_ident("Send") && !p.is_ident("Sync")) 75 | .collect(), 76 | ); 77 | 78 | Ok(()) 79 | })?; 80 | 81 | if let Some(ret) = ret { 82 | ml.tokens = ret.into_token_stream(); 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | Ok(()) 90 | } 91 | } 92 | 93 | impl Parse for Parsed { 94 | fn parse(input: ParseStream<'_>) -> ParseResult { 95 | let mut ty = input.parse::()?; 96 | 97 | let derive_send = ty.derives("Send"); 98 | let derive_sync = ty.derives("Sync"); 99 | 100 | Self::remove_uneeded_derives(&mut ty)?; 101 | 102 | let mut impls = Vec::new(); 103 | while !input.is_empty() { 104 | let mut impl_ = input.parse::()?; 105 | 106 | let ident = match &*impl_.self_ty { 107 | Type::Path(p) if p.qself.is_none() && p.path.leading_colon.is_none() && p.path.segments.len() == 1 => &p.path.segments.last().unwrap().ident, 108 | _ => return Err(input.error("Expected trait implementation for a simple type that is in the scope of the current module!")), 109 | }; 110 | 111 | if ty.is_unknown() { 112 | ty = TypeToMock::Extern { 113 | ident: ident.clone(), 114 | generics: impl_.generics.clone(), 115 | }; 116 | } else if ty.ident() != ident { 117 | return Err(input.error("Implementing mock traits for different type in the same mock!{} block is not supported!")); 118 | } 119 | 120 | Self::add_default_impl(&mut impl_)?; 121 | 122 | impls.push(impl_); 123 | } 124 | 125 | Ok(Self { 126 | ty, 127 | impls, 128 | derive_send, 129 | derive_sync, 130 | }) 131 | } 132 | } 133 | 134 | impl ToTokens for Parsed { 135 | fn to_tokens(&self, tokens: &mut TokenStream) { 136 | self.ty.to_tokens(tokens); 137 | 138 | if !self.ty.is_extern() { 139 | for i in &self.impls { 140 | i.clone().remove_murf_attrs().to_tokens(tokens); 141 | } 142 | } 143 | } 144 | } 145 | 146 | /// Object the mock is implemented for 147 | pub(crate) enum TypeToMock { 148 | Enum(ItemEnum), 149 | Struct(ItemStruct), 150 | Extern { ident: Ident, generics: Generics }, 151 | Unknown { ident: Ident, generics: Generics }, 152 | } 153 | 154 | impl TypeToMock { 155 | pub(crate) fn is_unknown(&self) -> bool { 156 | matches!(self, Self::Unknown { .. }) 157 | } 158 | 159 | pub(crate) fn is_extern(&self) -> bool { 160 | matches!(self, Self::Extern { .. }) 161 | } 162 | 163 | pub(crate) fn ident(&self) -> &Ident { 164 | match self { 165 | Self::Enum(o) => &o.ident, 166 | Self::Struct(o) => &o.ident, 167 | Self::Extern { ident, .. } | Self::Unknown { ident, .. } => ident, 168 | } 169 | } 170 | 171 | pub(crate) fn generics(&self) -> &Generics { 172 | match self { 173 | Self::Enum(o) => &o.generics, 174 | Self::Struct(o) => &o.generics, 175 | Self::Extern { generics, .. } | Self::Unknown { generics, .. } => generics, 176 | } 177 | } 178 | } 179 | 180 | impl AttribsEx for TypeToMock { 181 | fn derives(&self, ident: &str) -> bool { 182 | match self { 183 | Self::Enum(o) => o.derives(ident), 184 | Self::Struct(o) => o.derives(ident), 185 | _ => false, 186 | } 187 | } 188 | 189 | fn has_murf_attr(&self, ident: &str) -> bool { 190 | match self { 191 | Self::Enum(o) => o.has_murf_attr(ident), 192 | Self::Struct(o) => o.has_murf_attr(ident), 193 | _ => false, 194 | } 195 | } 196 | 197 | fn remove_murf_attrs(self) -> Self { 198 | match self { 199 | Self::Enum(o) => Self::Enum(o.remove_murf_attrs()), 200 | Self::Struct(o) => Self::Struct(o.remove_murf_attrs()), 201 | x => x, 202 | } 203 | } 204 | } 205 | 206 | impl Parse for TypeToMock { 207 | fn parse(input: ParseStream<'_>) -> ParseResult { 208 | let fork = input.fork(); 209 | let ret = match fork.parse::()? { 210 | Item::Enum(o) => Ok(Self::Enum(o)), 211 | Item::Struct(o) => Ok(Self::Struct(o)), 212 | Item::Impl(i) => { 213 | if let Type::Path(p) = &*i.self_ty { 214 | return Ok(Self::Unknown { 215 | ident: p.path.segments.last().unwrap().ident.clone(), 216 | generics: i.generics, 217 | }); 218 | } 219 | 220 | Err(input.error("Unexpected type!")) 221 | } 222 | _ => Err(input.error("Expected enum, struct or impl block!")), 223 | }; 224 | 225 | if ret.is_ok() { 226 | input.advance_to(&fork); 227 | } 228 | 229 | ret 230 | } 231 | } 232 | 233 | impl ToTokens for TypeToMock { 234 | fn to_tokens(&self, tokens: &mut TokenStream) { 235 | match self { 236 | Self::Enum(o) => o.to_tokens(tokens), 237 | Self::Struct(o) => o.to_tokens(tokens), 238 | _ => (), 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /murf-macros/src/mock/shared.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::{quote, ToTokens}; 3 | 4 | use crate::{misc::GenericsEx, mock::context::MethodContextData}; 5 | 6 | use super::context::{Context, ContextData, MethodContext}; 7 | 8 | pub(crate) struct Shared { 9 | context: Context, 10 | expectations: Vec, 11 | } 12 | 13 | impl Shared { 14 | pub(crate) fn new(context: Context) -> Self { 15 | Self { 16 | context, 17 | expectations: Vec::new(), 18 | } 19 | } 20 | 21 | pub(crate) fn add_expectation(&mut self, context: MethodContext) { 22 | self.expectations.push(context); 23 | } 24 | } 25 | 26 | impl ToTokens for Shared { 27 | fn to_tokens(&self, tokens: &mut TokenStream) { 28 | let Self { 29 | context, 30 | expectations, 31 | } = self; 32 | 33 | let ContextData { 34 | ident_murf, 35 | ident_state, 36 | ga_mock, 37 | trait_send, 38 | trait_sync, 39 | .. 40 | } = &**context; 41 | 42 | let ga_mock_phantom = ga_mock.make_phantom_data(); 43 | let (ga_mock_impl, ga_mock_types, ga_mock_where) = ga_mock.split_for_impl(); 44 | 45 | let expectation_field_defs = expectations.iter().map(|cx| { 46 | let field = &cx.ident_expectation_field; 47 | 48 | if cx.is_associated { 49 | quote! { 50 | #field: Vec>>> 51 | } 52 | } else { 53 | quote! { 54 | #field: Vec> 55 | } 56 | } 57 | }); 58 | 59 | let expectation_field_ctor = expectations.iter().map(|cx| { 60 | let field = &cx.ident_expectation_field; 61 | 62 | quote! { 63 | #field: Vec::new() 64 | } 65 | }); 66 | 67 | let expectation_err = format!("Mocked object '{ident_state}' has unfulfilled expectations"); 68 | 69 | let expectations = expectations.iter().map(|cx| { 70 | let MethodContextData { 71 | is_associated, 72 | ident_expectation_field, 73 | ident_expectation_module, 74 | .. 75 | } = &**cx; 76 | 77 | let expectation_unwrap = is_associated.then(|| { 78 | quote! { 79 | let ex = ex.lock(); 80 | } 81 | }); 82 | 83 | let expectation_cleanup = is_associated.then(|| { 84 | quote! { 85 | #ident_expectation_module::cleanup_associated_expectations(); 86 | } 87 | }); 88 | 89 | quote! { 90 | for ex in &self.#ident_expectation_field { 91 | #expectation_unwrap 92 | if ex.is_ready() { 93 | ex.set_done(); 94 | } else { 95 | if !raise { 96 | println!(); 97 | println!(#expectation_err); 98 | raise = true; 99 | } 100 | 101 | println!("- {}", &ex); 102 | } 103 | } 104 | 105 | self.#ident_expectation_field.clear(); 106 | 107 | #expectation_cleanup 108 | } 109 | }); 110 | 111 | tokens.extend(quote! { 112 | /// State that is shared between the different helper objects. 113 | pub struct Shared #ga_mock_types #ga_mock_where { 114 | #( #expectation_field_defs, )* 115 | _marker: #ga_mock_phantom, 116 | } 117 | 118 | impl #ga_mock_impl Shared #ga_mock_types #ga_mock_where { 119 | pub(super) fn checkpoint(&mut self) { 120 | let mut raise = false; 121 | 122 | #( #expectations )* 123 | 124 | if raise { 125 | println!(); 126 | panic!(#expectation_err); 127 | } 128 | } 129 | } 130 | 131 | impl #ga_mock_impl Default for Shared #ga_mock_types #ga_mock_where { 132 | fn default() -> Self { 133 | Self { 134 | #( #expectation_field_ctor, )* 135 | _marker: PhantomData, 136 | } 137 | } 138 | } 139 | 140 | impl #ga_mock_impl Debug for Shared #ga_mock_types #ga_mock_where { 141 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 142 | write!(f, "Shared") 143 | } 144 | } 145 | }); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /murf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "murf" 3 | version = "0.2.0" 4 | edition = "2021" 5 | description = "A mocking and unit test framework for rust" 6 | license = "MIT" 7 | homepage = "https://github.com/peeriot/murf" 8 | readme = "README.md" 9 | keywords = [ "mock", "mocking", "test", "testing" ] 10 | categories = [ "development-tools::testing" ] 11 | 12 | [dependencies] 13 | murf-macros = "0.2" 14 | once_cell = { version = "1.18", features = [ "parking_lot" ] } 15 | parking_lot = "0.12" 16 | parse_duration = "2.1" 17 | 18 | [dev-dependencies] 19 | futures = "0.3" 20 | murf-macros = { version = "0.2", features = [ "force-name" ] } 21 | -------------------------------------------------------------------------------- /murf/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /murf/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /murf/src/action/invoke.rs: -------------------------------------------------------------------------------- 1 | use super::Action; 2 | 3 | /// Creates a [`Invoke`] action that forwards the call to the passed function `f`. 4 | pub fn invoke(func: F) -> Invoke { 5 | Invoke(func) 6 | } 7 | 8 | /// Action that forwards the call to the passed function `F`. 9 | #[derive(Debug)] 10 | pub struct Invoke(pub F); 11 | 12 | impl Action for Invoke 13 | where 14 | F: FnOnce(X) -> T, 15 | { 16 | fn exec(self, args: X) -> T { 17 | (self.0)(args) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /murf/src/action/mod.rs: -------------------------------------------------------------------------------- 1 | //! The [`action`](self) module contains difference pre-defined actions that may 2 | //! be executed for a call-expectation of a mocked type. 3 | 4 | mod invoke; 5 | mod returns; 6 | 7 | pub use invoke::{invoke, Invoke}; 8 | pub use returns::{return_, return_pointee, return_ref, Return, ReturnPointee, ReturnRef}; 9 | 10 | /// Trait that defines an action that can only be executed once. 11 | /// 12 | /// This is similar to [`FnOnce`] of the standard library. 13 | /// 14 | /// The arguments passed to the action are either 15 | /// - a unit `()` for no arguments 16 | /// - a single type `T` for one argument 17 | /// - or a tuple `(T1, T2, ...)` of many arguments 18 | pub trait Action { 19 | /// Execute the action with the passed arguments. 20 | fn exec(self, args: T) -> R; 21 | } 22 | 23 | impl Action for X 24 | where 25 | X: FnOnce(T) -> R, 26 | { 27 | fn exec(self, args: T) -> R { 28 | self(args) 29 | } 30 | } 31 | 32 | /// Like [`Action`] but this action may be called repeatedly. 33 | /// 34 | /// This is similar to [`FnMut`] of the standard library. 35 | pub trait RepeatableAction { 36 | /// Execute the action with the passed arguments. 37 | fn exec(&mut self, args: T) -> R; 38 | } 39 | 40 | impl RepeatableAction for X 41 | where 42 | X: FnMut(T) -> R, 43 | { 44 | fn exec(&mut self, args: T) -> R { 45 | self(args) 46 | } 47 | } 48 | 49 | /// Helper type to implement [`RepeatableAction`] for a action that can only be 50 | /// called once. Any further call will panic! 51 | #[derive(Debug)] 52 | pub struct OnetimeAction(Option); 53 | 54 | impl OnetimeAction { 55 | /// Create a new [`OnetimeAction`] instance. 56 | pub fn new(inner: X) -> Self { 57 | Self(Some(inner)) 58 | } 59 | } 60 | 61 | impl RepeatableAction for OnetimeAction 62 | where 63 | X: Action, 64 | { 65 | fn exec(&mut self, args: T) -> R { 66 | self.0 67 | .take() 68 | .expect("Action was already executed") 69 | .exec(args) 70 | } 71 | } 72 | 73 | /// Helper type to implement [`RepeatableAction`] for any action that implements 74 | /// [`Action`] and [`Clone`]. 75 | #[derive(Debug)] 76 | pub struct RepeatedAction(X); 77 | 78 | impl RepeatedAction { 79 | /// Create a new [`RepeatedAction`] instance. 80 | pub fn new(inner: X) -> Self { 81 | Self(inner) 82 | } 83 | } 84 | 85 | impl RepeatableAction for RepeatedAction 86 | where 87 | X: Action + Clone, 88 | { 89 | fn exec(&mut self, args: T) -> R { 90 | self.0.clone().exec(args) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /murf/src/action/returns.rs: -------------------------------------------------------------------------------- 1 | use crate::Pointee; 2 | 3 | use super::Action; 4 | 5 | /// Creates a [`Return`] action that returns the passed `value` when called. 6 | pub fn return_(value: T) -> Return { 7 | Return(value) 8 | } 9 | 10 | /// Action that returns the passed value `T` when called. 11 | #[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] 12 | pub struct Return(pub T); 13 | 14 | impl Action for Return { 15 | fn exec(self, _args: X) -> T { 16 | self.0 17 | } 18 | } 19 | 20 | /// Create a [`ReturnRef`] action that returns a reference to the passed `value` 21 | /// when called. 22 | pub fn return_ref(value: &T) -> ReturnRef<'_, T> { 23 | ReturnRef(value) 24 | } 25 | 26 | /// Action that returns the passed reference `&T` when called. 27 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] 28 | pub struct ReturnRef<'a, T>(pub &'a T); 29 | 30 | impl<'a, T, X> Action for ReturnRef<'a, T> { 31 | fn exec(self, _args: X) -> &'a T { 32 | self.0 33 | } 34 | } 35 | 36 | /// Creates a [`ReturnPointee`] action that returns the value the [`Pointee`] 37 | /// points to when called. 38 | pub fn return_pointee(value: T) -> ReturnPointee { 39 | ReturnPointee(value) 40 | } 41 | 42 | /// Action that returns the value the [`Pointee`] points to when called. 43 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] 44 | pub struct ReturnPointee(pub T); 45 | 46 | impl Action for ReturnPointee

47 | where 48 | P: Pointee, 49 | { 50 | fn exec(self, _args: X) -> T { 51 | self.0.get() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /murf/src/example.rs: -------------------------------------------------------------------------------- 1 | //! Contains a generated example for the mocked types. 2 | 3 | #![allow(unused)] 4 | 5 | use crate::mock; 6 | 7 | /// Simple test trait to generate a mocked version for. 8 | pub trait Fuu { 9 | /// Simple method to generate a mocked version for. 10 | fn fuu(&self, x: usize) -> usize; 11 | } 12 | 13 | mock! { 14 | /// Type the implements the [`Fuu`] trait. 15 | #[derive(Default, Debug)] 16 | pub struct MyStruct; 17 | 18 | impl Fuu for MyStruct { 19 | fn fuu(&self, x: usize) -> usize; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /murf/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn( 2 | clippy::pedantic, 3 | future_incompatible, 4 | missing_debug_implementations, 5 | missing_docs, 6 | nonstandard_style, 7 | rust_2018_idioms, 8 | rust_2021_compatibility, 9 | unused 10 | )] 11 | #![doc = include_str!("../README.md")] 12 | #![allow(clippy::module_name_repetitions, clippy::needless_doctest_main)] 13 | 14 | pub mod action; 15 | pub mod local_context; 16 | pub mod matcher; 17 | pub mod misc; 18 | pub mod sequence; 19 | pub mod times; 20 | pub mod types; 21 | 22 | #[cfg(doc)] 23 | pub mod example; 24 | 25 | pub use murf_macros::{expect_call, expect_method_call, mock}; 26 | pub use once_cell::sync::Lazy; 27 | 28 | pub use action::Action; 29 | pub use local_context::LocalContext; 30 | pub use matcher::Matcher; 31 | pub use misc::{next_type_id, Expectation, Pointee, Pointer}; 32 | pub use sequence::{InSequence, Sequence, SequenceHandle}; 33 | pub use times::{Times, TimesRange}; 34 | -------------------------------------------------------------------------------- /murf/src/local_context.rs: -------------------------------------------------------------------------------- 1 | //! The [`local_context`](self) module implements the [`LocalContext`] type. 2 | 3 | use std::cell::RefCell; 4 | use std::collections::HashMap; 5 | use std::marker::PhantomData; 6 | use std::rc::Rc; 7 | use std::sync::Weak; 8 | 9 | use parking_lot::Mutex; 10 | 11 | use crate::Expectation; 12 | 13 | /// Type that can be used to manage and store call expectations to a associated 14 | /// or static function in a thread local context. If no [`LocalContext`] is 15 | /// provided when the expectation was defined the expectation is stored in the 16 | /// global expectation context. 17 | /// 18 | /// Local contexts may be useful if you want to use `murf` in a threaded environment 19 | /// to execute tests in parallel. Using the global context the expectations of 20 | /// multiple tests may be mixed which result in undefined behavior. 21 | #[must_use] 22 | #[derive(Debug)] 23 | pub struct LocalContext { 24 | _marker: PhantomData<()>, 25 | } 26 | 27 | /// Actual state that is stored for the [`LocalContext`]. 28 | #[derive(Debug)] 29 | pub struct Inner { 30 | parent: Option>, 31 | expectations: HashMap>, 32 | } 33 | 34 | type WeakException = Weak>>; 35 | 36 | impl LocalContext { 37 | /// Create a new [`LocalContext`] instance. 38 | pub fn new() -> Self { 39 | CURRENT_CONTEXT.with(|cell| { 40 | let mut cell = cell.borrow_mut(); 41 | let parent = cell.take().map(Box::new); 42 | 43 | *cell = Some(Inner { 44 | parent, 45 | expectations: HashMap::new(), 46 | }); 47 | }); 48 | 49 | Self { 50 | _marker: PhantomData, 51 | } 52 | } 53 | 54 | /// Get the state of the current local context. 55 | pub fn current() -> Rc>> { 56 | CURRENT_CONTEXT.with(Clone::clone) 57 | } 58 | } 59 | 60 | impl Default for LocalContext { 61 | fn default() -> Self { 62 | Self::new() 63 | } 64 | } 65 | 66 | impl Drop for LocalContext { 67 | fn drop(&mut self) { 68 | CURRENT_CONTEXT.with(|cell| { 69 | let mut cell = cell.borrow_mut(); 70 | *cell = cell.take().unwrap().parent.map(|x| *x); 71 | }); 72 | } 73 | } 74 | 75 | impl Inner { 76 | /// Return a iterator of expectations that are defined in the current thread local context. 77 | pub fn expectations(&self, type_id: usize) -> impl Iterator + '_ { 78 | let parent: Box> = Box::new( 79 | self.parent 80 | .as_ref() 81 | .into_iter() 82 | .flat_map(move |p| p.expectations(type_id)), 83 | ); 84 | 85 | self.expectations 86 | .get(&type_id) 87 | .into_iter() 88 | .flat_map(|x| x.iter()) 89 | .chain(parent) 90 | } 91 | 92 | /// Add a expectation the the current thread local context. 93 | pub fn push(&mut self, type_id: usize, expectation: WeakException) { 94 | self.expectations 95 | .entry(type_id) 96 | .or_default() 97 | .push(expectation); 98 | } 99 | } 100 | 101 | thread_local! { 102 | static CURRENT_CONTEXT: Rc>> = Rc::new(RefCell::new(None)); 103 | } 104 | -------------------------------------------------------------------------------- /murf/src/matcher/any.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result as FmtResult}; 2 | 3 | use crate::Matcher; 4 | 5 | /// Create a [`Any`] matcher that matches any argument. 6 | pub fn any() -> Any { 7 | Any 8 | } 9 | 10 | /// Implements a [`Matcher`] that matches any argument. 11 | #[must_use] 12 | #[derive(Debug)] 13 | pub struct Any; 14 | 15 | impl Matcher for Any { 16 | fn matches(&self, _value: &T) -> bool { 17 | true 18 | } 19 | } 20 | 21 | impl Display for Any { 22 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 23 | write!(f, "any") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /murf/src/matcher/closure.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result as FmtResult}; 2 | 3 | use crate::Matcher; 4 | 5 | /// Create a new [`Closure`] matcher that executes the passed function `f` to 6 | /// verify if a argument matches the expectation. 7 | pub fn closure(f: F) -> Closure { 8 | Closure(f) 9 | } 10 | 11 | /// Implements a [`Matcher`] that executes the passed function `F` to 12 | /// verify if a argument matches the expectation. 13 | #[must_use] 14 | #[derive(Debug)] 15 | pub struct Closure(pub F); 16 | 17 | impl Matcher for Closure 18 | where 19 | F: Fn(&T) -> bool, 20 | { 21 | fn matches(&self, value: &T) -> bool { 22 | self.0(value) 23 | } 24 | } 25 | 26 | impl Display for Closure { 27 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 28 | write!(f, "Closure") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /murf/src/matcher/compare.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; 2 | 3 | use super::Matcher; 4 | 5 | macro_rules! impl_matcher { 6 | ($type:ident, $trait:ident::$method:ident, $fmt:tt, $ctor_doc:expr, $type_doc:expr) => { 7 | #[doc = $ctor_doc] 8 | pub fn $method(value: T) -> $type { 9 | $type(value) 10 | } 11 | 12 | #[must_use] 13 | #[derive(Debug)] 14 | #[doc = $type_doc] 15 | pub struct $type(pub T); 16 | 17 | impl Matcher for $type 18 | where 19 | T: $trait + Debug, 20 | { 21 | fn matches(&self, value: &X) -> bool { 22 | self.0.$method(value) 23 | } 24 | } 25 | 26 | impl Display for $type 27 | where 28 | T: Debug, 29 | { 30 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 31 | write!(f, $fmt, self.0) 32 | } 33 | } 34 | }; 35 | } 36 | 37 | impl_matcher!( 38 | Eq, 39 | PartialEq::eq, 40 | "Eq({:?})", 41 | "Create a new [`Eq`](struct@Eq) matcher that checks if a argument is equal to the passed `value`.", 42 | "Implements a [`Matcher`] that checks if a argument is equal to the passed value `T`." 43 | ); 44 | impl_matcher!( 45 | Ne, 46 | PartialEq::ne, 47 | "Ne({:?})", 48 | "Create a new [`Ne`] matcher that checks if a argument is not equal to the passed `value`.", 49 | "Implements a [`Matcher`] that checks if a argument is not equal to the passed value `T`." 50 | ); 51 | impl_matcher!( 52 | Lt, 53 | PartialOrd::lt, 54 | "Lt({:?})", 55 | "Create a new [`Lt`] matcher that checks if a argument is lower than the passed `value`.", 56 | "Implements a [`Matcher`] that checks if a argument is lower than the passed value `T`." 57 | ); 58 | impl_matcher!( 59 | Le, 60 | PartialOrd::le, 61 | "Le({:?})", 62 | "Create a new [`Le`] matcher that checks if a argument is lower or equal to the passed `value`.", 63 | "Implements a [`Matcher`] that checks if a argument is lower or equal to the passed value `T`." 64 | ); 65 | impl_matcher!( 66 | Gt, 67 | PartialOrd::gt, 68 | "Gt({:?})", 69 | "Create a new [`Gt`] matcher that checks if a argument is greater than the passed `value`.", 70 | "Implements a [`Matcher`] that checks if a argument is greater than the passed value `T`." 71 | ); 72 | impl_matcher!( 73 | Ge, 74 | PartialOrd::ge, 75 | "Ge({:?})", "Create a new [`Ge`] matcher that checks if a argument is greater or equal to the passed `value`.", 76 | "Implements a [`Matcher`] that checks if a argument is greater or equal to the passed value `T`." 77 | ); 78 | -------------------------------------------------------------------------------- /murf/src/matcher/deref.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result as FmtResult}; 2 | 3 | use crate::Matcher; 4 | 5 | /// Create a new [`Deref`] matcher, that calls the [`deref`](std::ops::Deref::deref()) 6 | /// method of the argument and forwards it to the passed `inner` matcher. 7 | pub fn deref(inner: M) -> Deref { 8 | Deref(inner) 9 | } 10 | 11 | /// Implements a [`Matcher`] that calls the [`deref`](std::ops::Deref::deref()) 12 | /// method of the argument and forwards it to the passed matcher `M`. 13 | #[must_use] 14 | #[derive(Debug)] 15 | pub struct Deref(pub M); 16 | 17 | impl Matcher for Deref 18 | where 19 | T: std::ops::Deref, 20 | T::Target: Sized, 21 | M: Matcher, 22 | { 23 | fn matches(&self, value: &T) -> bool { 24 | self.0.matches(&**value) 25 | } 26 | } 27 | 28 | impl Display for Deref 29 | where 30 | M: Display, 31 | { 32 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 33 | write!(f, "deref(")?; 34 | self.0.fmt(f)?; 35 | write!(f, ")")?; 36 | 37 | Ok(()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /murf/src/matcher/inspect.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; 2 | 3 | use crate::Matcher; 4 | 5 | /// Creates a new [`Inspect`] matcher that prints the argument to stdout and then 6 | /// forwards it to the passed `inner` matcher. 7 | pub fn inspect(inner: M) -> Inspect { 8 | Inspect(inner) 9 | } 10 | 11 | /// implements a [`Matcher`] that prints the argument to stdout and then forwards 12 | /// it to the passed inner matcher `M`. 13 | #[must_use] 14 | #[derive(Debug)] 15 | pub struct Inspect(pub M); 16 | 17 | impl Matcher for Inspect 18 | where 19 | T: Debug, 20 | M: Matcher, 21 | { 22 | fn matches(&self, value: &T) -> bool { 23 | println!( 24 | "{}", 25 | FormatHelper { 26 | matcher: &self.0, 27 | value 28 | } 29 | ); 30 | 31 | self.0.matches(value) 32 | } 33 | } 34 | 35 | impl Display for Inspect 36 | where 37 | M: Display, 38 | { 39 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 40 | self.0.fmt(f) 41 | } 42 | } 43 | 44 | struct FormatHelper<'a, M, T> { 45 | matcher: &'a M, 46 | value: &'a T, 47 | } 48 | 49 | impl<'a, M, T> Display for FormatHelper<'a, M, T> 50 | where 51 | M: Matcher, 52 | T: Debug, 53 | { 54 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 55 | write!(f, "Expect ")?; 56 | self.matcher.fmt(f)?; 57 | write!(f, " to match {:?}", self.value)?; 58 | 59 | Ok(()) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /murf/src/matcher/mod.rs: -------------------------------------------------------------------------------- 1 | //! The [`matcher`](self) module define different [`Matcher`]s that can be used 2 | //! to check the arguments of a call expectation. 3 | 4 | mod any; 5 | mod closure; 6 | mod compare; 7 | mod deref; 8 | mod inspect; 9 | mod multi; 10 | mod no_args; 11 | mod range; 12 | mod string; 13 | 14 | use std::fmt::Display; 15 | 16 | pub use any::{any, Any}; 17 | pub use closure::{closure, Closure}; 18 | pub use compare::{eq, ge, gt, le, lt, ne, Eq, Ge, Gt, Le, Lt, Ne}; 19 | pub use deref::{deref, Deref}; 20 | pub use inspect::{inspect, Inspect}; 21 | 22 | pub use multi::{multi, Multi}; 23 | pub use no_args::{no_args, NoArgs}; 24 | pub use range::{range, Range}; 25 | pub use string::{ 26 | contains as str_contains, ends_with as str_ends_with, is_empty, starts_with as str_starts_with, 27 | Contains as StrContains, EndsWith as StrEndsWith, IsEmpty, StartsWith as StrStartsWith, 28 | }; 29 | 30 | /// A matcher is used to check if the passed argument matches a pre-defined 31 | /// expectation. It is mostly used to verify the arguments to an expected call. 32 | pub trait Matcher: Display { 33 | /// Returns `true` if the passed `value` matches the expectations, `false` 34 | /// otherwise. 35 | fn matches(&self, value: &T) -> bool; 36 | } 37 | -------------------------------------------------------------------------------- /murf/src/matcher/multi.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result as FmtResult}; 2 | use std::mem::take; 3 | 4 | use crate::Matcher; 5 | 6 | /// Creates a new [`Multi`] matcher that checks a tuple of parameters against 7 | /// the passed tuple of `inner` matchers. 8 | pub fn multi(inner: T) -> Multi { 9 | Multi(inner) 10 | } 11 | 12 | /// Implements a [`Matcher`] that checks a tuple of parameters against the passed 13 | /// inner tuple of matchers `T`. 14 | /// 15 | /// `T` has to be a valid tuple between two and ten arguments: 16 | /// - `(T1, T2)` 17 | /// - `(T1, ..., T10)` 18 | #[must_use] 19 | #[derive(Debug)] 20 | pub struct Multi(T); 21 | 22 | macro_rules! impl_multi { 23 | (($( $arg_name:ident: $arg_type:ident ),+) => ($( $matcher_name:ident: $matcher_type:ident ),+)) => { 24 | #[allow(unused_parens)] 25 | impl<$( $arg_type ),+ $( , $matcher_type )+> Matcher<($( $arg_type ),+)> for Multi<($( $matcher_type ),+)> 26 | where 27 | $( 28 | $matcher_type: Matcher<$arg_type>, 29 | )+ 30 | { 31 | fn matches(&self, ($( $arg_name ),+): &($( $arg_type ),+)) -> bool { 32 | let Self(($( $matcher_name ),+)) = self; 33 | 34 | $( 35 | $matcher_name.matches($arg_name) 36 | )&&+ 37 | } 38 | } 39 | 40 | #[allow(unused_parens)] 41 | impl<$( $matcher_type ),+> Display for Multi<($( $matcher_type ),+)> 42 | where 43 | $( 44 | $matcher_type: Display, 45 | )+ 46 | { 47 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 48 | let mut first = true; 49 | let Self(($( $matcher_name ),+)) = self; 50 | 51 | $( 52 | if !take(&mut first) { 53 | write!(f, ", ")?; 54 | } 55 | 56 | $matcher_name.fmt(f)?; 57 | )+ 58 | 59 | Ok(()) 60 | } 61 | } 62 | }; 63 | } 64 | 65 | impl_multi!((a0: T0, a1: T1) => (m0: M0, m1: M1)); 66 | impl_multi!((a0: T0, a1: T1, a2: T2) => (m0: M0, m1: M1, m2: M2)); 67 | impl_multi!((a0: T0, a1: T1, a2: T2, a3: T3) => (m0: M0, m1: M1, m2: M2, m3: M3)); 68 | impl_multi!((a0: T0, a1: T1, a2: T2, a3: T3, a4: T4) => (m0: M0, m1: M1, m2: M2, m3: M3, m4: M4)); 69 | impl_multi!((a0: T0, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => (m0: M0, m1: M1, m2: M2, m3: M3, m4: M4, m5: M5)); 70 | impl_multi!((a0: T0, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => (m0: M0, m1: M1, m2: M2, m3: M3, m4: M4, m5: M5, m6: M6)); 71 | impl_multi!((a0: T0, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7) => (m0: M0, m1: M1, m2: M2, m3: M3, m4: M4, m5: M5, m6: M6, m7: M7)); 72 | impl_multi!((a0: T0, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8) => (m0: M0, m1: M1, m2: M2, m3: M3, m4: M4, m5: M5, m6: M6, m7: M7, m8: M8)); 73 | impl_multi!((a0: T0, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6, a7: T7, a8: T8, a9: T9) => (m0: M0, m1: M1, m2: M2, m3: M3, m4: M4, m5: M5, m6: M6, m7: M7, m8: M8, m9: M9)); 74 | -------------------------------------------------------------------------------- /murf/src/matcher/no_args.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result as FmtResult}; 2 | 3 | use crate::Matcher; 4 | 5 | /// Creates a new [`NoArgs`] matcher, that matches only the empty parameter list 6 | /// `()` and always returns `true`. 7 | pub fn no_args() -> NoArgs { 8 | NoArgs 9 | } 10 | 11 | /// Implements a [`Matcher`], that matches only the empty parameter list `()` and 12 | /// always returns `true`. 13 | #[must_use] 14 | #[derive(Debug)] 15 | pub struct NoArgs; 16 | 17 | impl Matcher<()> for NoArgs { 18 | fn matches(&self, _value: &()) -> bool { 19 | true 20 | } 21 | } 22 | 23 | impl Display for NoArgs { 24 | fn fmt(&self, _: &mut Formatter<'_>) -> FmtResult { 25 | Ok(()) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /murf/src/matcher/range.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result as FmtResult}; 2 | use std::marker::PhantomData; 3 | use std::ops::{Bound, RangeBounds}; 4 | 5 | use crate::Matcher; 6 | 7 | /// Creates a new [`Range`] matcher that matches if the argument it is in the passed 8 | /// `range`. 9 | pub fn range(range: R) -> Range 10 | where 11 | R: RangeBounds, 12 | { 13 | Range::new(range) 14 | } 15 | 16 | /// Implements a [`Matcher`], that matches if the argument is in the passed range `R`. 17 | #[must_use] 18 | #[derive(Debug)] 19 | pub struct Range { 20 | range: R, 21 | _marker: PhantomData, 22 | } 23 | 24 | impl Range { 25 | /// Create a new [`Range`] matcher instance. 26 | pub fn new(range: R) -> Self { 27 | Self { 28 | range, 29 | _marker: PhantomData, 30 | } 31 | } 32 | } 33 | 34 | impl Matcher for Range 35 | where 36 | R: RangeBounds, 37 | T: PartialOrd + Display, 38 | U: PartialOrd, 39 | { 40 | fn matches(&self, value: &U) -> bool { 41 | self.range.contains(value) 42 | } 43 | } 44 | 45 | impl Display for Range 46 | where 47 | R: RangeBounds, 48 | T: Display, 49 | { 50 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 51 | match self.range.start_bound() { 52 | Bound::Unbounded => write!(f, "[_, "), 53 | Bound::Included(x) => write!(f, "[{x}, "), 54 | Bound::Excluded(x) => write!(f, "({x}, "), 55 | }?; 56 | 57 | match self.range.end_bound() { 58 | Bound::Unbounded => write!(f, "_]"), 59 | Bound::Included(x) => write!(f, "{x}]"), 60 | Bound::Excluded(x) => write!(f, "{x})"), 61 | }?; 62 | 63 | Ok(()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /murf/src/matcher/string.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result as FmtResult}; 2 | 3 | use super::Matcher; 4 | 5 | /// Create a new [`IsEmpty`] matcher, that matches any kind of string, that is empty. 6 | pub fn is_empty() -> IsEmpty { 7 | IsEmpty 8 | } 9 | 10 | /// Implements a [`Matcher`] that matches any kind of string that is empty. 11 | #[must_use] 12 | #[derive(Debug)] 13 | pub struct IsEmpty; 14 | 15 | impl Matcher for IsEmpty 16 | where 17 | X: AsRef, 18 | { 19 | fn matches(&self, value: &X) -> bool { 20 | value.as_ref().is_empty() 21 | } 22 | } 23 | 24 | impl Display for IsEmpty { 25 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 26 | write!(f, "IsEmpty") 27 | } 28 | } 29 | 30 | macro_rules! impl_str_matcher { 31 | ($type:ident, str::$method:ident, $fmt:tt, $ctor_doc:expr, $type_doc:expr) => { 32 | #[doc = $ctor_doc] 33 | pub fn $method>(pattern: P) -> $type { 34 | $type(pattern.into()) 35 | } 36 | 37 | #[derive(Debug)] 38 | #[doc = $type_doc] 39 | pub struct $type(String); 40 | 41 | impl Matcher for $type 42 | where 43 | X: AsRef, 44 | { 45 | fn matches(&self, value: &X) -> bool { 46 | value.as_ref().$method(&self.0) 47 | } 48 | } 49 | 50 | impl Display for $type { 51 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 52 | write!(f, $fmt, self.0) 53 | } 54 | } 55 | }; 56 | } 57 | 58 | impl_str_matcher!( 59 | StartsWith, 60 | str::starts_with, 61 | "StartsWith({})", 62 | "Create a new [`StartsWith`] matcher, that matches any kind of string, that starts with the passed `pattern`.", 63 | "Implements a [`Matcher'] that matches any kind of string, that starts with the passed pattern." 64 | ); 65 | impl_str_matcher!( 66 | EndsWith, 67 | str::ends_with, 68 | "EndsWith({})", 69 | "Create a new [`EndsWith`] matcher, that matches any kind of string, that ends with the passed `pattern`.", 70 | "Implements a [`EndsWith'] that matches any kind of string, that starts with the passed pattern." 71 | ); 72 | impl_str_matcher!( 73 | Contains, 74 | str::contains, 75 | "Contains({})", 76 | "Create a new [`Contains`] matcher, that matches any kind of string, that contains the passed `pattern`.", 77 | "Implements a [`Contains'] that matches any kind of string, that starts with the passed pattern." 78 | ); 79 | -------------------------------------------------------------------------------- /murf/src/misc.rs: -------------------------------------------------------------------------------- 1 | //! The [`misc`](self) crate contains different helper types and traits. 2 | 3 | use std::borrow::Borrow; 4 | use std::cell::RefCell; 5 | use std::fmt::Display; 6 | use std::rc::Rc; 7 | use std::sync::{ 8 | atomic::{AtomicUsize, Ordering}, 9 | Arc, Mutex, 10 | }; 11 | 12 | /// Helper type that is used to the values a pointer like type is pointing to. 13 | /// 14 | /// This is mostly used in the [`ReturnPointee`](crate::action::ReturnPointee) type. 15 | pub trait Pointee { 16 | /// Get the value the pointer type is currently pointing at. 17 | fn get(&self) -> T; 18 | } 19 | 20 | impl Pointee for Rc> 21 | where 22 | T: Clone, 23 | { 24 | fn get(&self) -> T { 25 | RefCell::borrow(self).clone() 26 | } 27 | } 28 | 29 | impl Pointee for Arc> 30 | where 31 | T: Clone, 32 | { 33 | fn get(&self) -> T { 34 | self.lock().unwrap().clone() 35 | } 36 | } 37 | 38 | /// Implements [`Pointee`] for any type `T` that implements [`Borrow`]. 39 | #[derive(Debug)] 40 | pub struct Borrowed(T); 41 | 42 | impl Pointee for Borrowed 43 | where 44 | X: Borrow, 45 | T: Clone, 46 | { 47 | fn get(&self) -> T { 48 | self.0.borrow().clone() 49 | } 50 | } 51 | 52 | /// Implements [`Pointee`] for any pointer type `* const T`. 53 | #[derive(Debug)] 54 | pub struct Pointer(pub *const T); 55 | 56 | impl Pointee for Pointer 57 | where 58 | T: Clone, 59 | { 60 | fn get(&self) -> T { 61 | unsafe { &*self.0 }.clone() 62 | } 63 | } 64 | 65 | /// Defines a expectation for a function call on a mocked object. 66 | pub trait Expectation: Display { 67 | /// Returns the type id of the expectation. 68 | fn type_id(&self) -> usize; 69 | 70 | /// Returns `true` if this expectation is ready, `false` otherwise. 71 | /// 72 | /// Ready means that the expectation was executed the expected amount of times. 73 | fn is_ready(&self) -> bool; 74 | 75 | /// Mark this expectation as done. 76 | /// 77 | /// Done means that this expectation has been finished and will not called again. 78 | fn set_done(&self); 79 | 80 | /// Get the type signature of the expectation. 81 | fn type_signature(&self) -> &'static str; 82 | } 83 | 84 | /// Get the next type id 85 | pub fn next_type_id() -> usize { 86 | NEXT_TYPE_ID.fetch_add(1, Ordering::Relaxed) 87 | } 88 | 89 | static NEXT_TYPE_ID: AtomicUsize = AtomicUsize::new(0); 90 | -------------------------------------------------------------------------------- /murf/src/sequence.rs: -------------------------------------------------------------------------------- 1 | //! The [`sequence`](self) module contains different types and helpers to manage 2 | //! the order of different call expectations. 3 | 4 | use std::cell::RefCell; 5 | use std::sync::atomic::{AtomicUsize, Ordering}; 6 | use std::sync::Arc; 7 | 8 | use parking_lot::{Mutex, MutexGuard}; 9 | 10 | /// A sequence is used to manage the order of call expectations. 11 | /// 12 | /// The sequence uses a so called [`SequenceHandle`] to keep track of registered 13 | /// elements in the sequence. The handle can be used to check the status of a 14 | /// particular element in the sequence. 15 | /// 16 | /// A sequence must explicitly be added to a expectation using the `in_sequence` 17 | /// or `add_sequence` method of the expectation object. 18 | #[must_use] 19 | #[derive(Default, Debug, Clone)] 20 | pub struct Sequence { 21 | inner: Arc>, 22 | } 23 | 24 | impl Sequence { 25 | /// Create a new empty sequence. 26 | pub fn new() -> Self { 27 | Self::default() 28 | } 29 | 30 | /// Create a new handle that is added to the end of the sequence. 31 | #[must_use] 32 | pub fn create_handle(&self) -> SequenceHandle { 33 | Inner::create_handle(self.inner.clone()) 34 | } 35 | } 36 | 37 | /// Represents a reserved element in a [`Sequence`]. 38 | /// 39 | /// The sequence handle can be used to check the status of a particular element 40 | /// in the sequence. 41 | /// 42 | /// A handle has different states: 43 | /// - Inactive: Other handles before the current one are not fulfilled yet. 44 | /// - Active: The current handle is the one that needs to be processed next. 45 | /// - Ready: The current handle has been called the expected amount of times, 46 | /// but may be called more times before marked as done. 47 | /// - Done: The handle is done and is not expected to be called again in 48 | /// the future. 49 | #[derive(Debug)] 50 | pub struct SequenceHandle { 51 | id: usize, 52 | inner: Arc>, 53 | sequence_id: usize, 54 | } 55 | 56 | impl SequenceHandle { 57 | /// Returns `true` if the current handle is active, `false` otherwise. 58 | #[must_use] 59 | pub fn is_active(&self) -> bool { 60 | self.inner.lock().is_active(self.id) 61 | } 62 | 63 | /// Returns `true` if the current handle is done, `false` otherwise. 64 | #[must_use] 65 | pub fn is_done(&self) -> bool { 66 | self.inner.lock().is_done(self.id) 67 | } 68 | 69 | /// Get the id of the sequence this handle belongs to. 70 | #[must_use] 71 | pub fn sequence_id(&self) -> usize { 72 | self.sequence_id 73 | } 74 | 75 | /// Mark the current handle as ready. 76 | pub fn set_ready(&self) { 77 | self.inner.lock().set_ready(self.id); 78 | } 79 | 80 | /// Mark the current handle as done. 81 | pub fn set_done(&self) { 82 | self.inner.lock().set_done(self.id); 83 | } 84 | 85 | /// Set the description of the handle. 86 | pub fn set_description(&self, value: String) { 87 | self.inner.lock().set_description(self.id, value); 88 | } 89 | 90 | /// Get an iterator of unsatisfied (not done yet) handles. 91 | #[must_use] 92 | pub fn unsatisfied(&self) -> Unsatisfied<'_> { 93 | Unsatisfied::new(self) 94 | } 95 | } 96 | 97 | impl Drop for SequenceHandle { 98 | fn drop(&mut self) { 99 | self.inner.lock().set_ready(self.id); 100 | } 101 | } 102 | 103 | /// [`Iterator`] over unsatisfied (not done yet) handles in a [`Sequence`]. 104 | #[derive(Debug)] 105 | pub struct Unsatisfied<'a> { 106 | guard: MutexGuard<'a, Inner>, 107 | id_end: usize, 108 | id_current: usize, 109 | } 110 | 111 | impl<'a> Unsatisfied<'a> { 112 | fn new(seq_handle: &'a SequenceHandle) -> Self { 113 | let guard = seq_handle.inner.lock(); 114 | let id_end = seq_handle.id; 115 | let id_current = guard.current_id; 116 | 117 | Self { 118 | guard, 119 | id_end, 120 | id_current, 121 | } 122 | } 123 | } 124 | 125 | impl<'a> Iterator for Unsatisfied<'a> { 126 | type Item = String; 127 | 128 | fn next(&mut self) -> Option { 129 | loop { 130 | if self.id_current == self.id_end { 131 | return None; 132 | } 133 | 134 | let meta = self.guard.items.get(self.id_current)?; 135 | self.id_current += 1; 136 | 137 | if !meta.is_ready { 138 | return Some(meta.description.clone()); 139 | } 140 | } 141 | } 142 | } 143 | 144 | /// Same like [`Sequence`] with the different that all expectations that are added 145 | /// after the [`InSequence`] has been defined are added to this sequence automatically. 146 | /// 147 | /// [`InSequence`] is thread local. 148 | #[derive(Debug)] 149 | pub struct InSequence { 150 | parent: Option>>, 151 | } 152 | 153 | impl InSequence { 154 | /// Create a new [`InSequence`] instance that automatically adds expectations 155 | /// to the passed `sequence`. 156 | #[must_use] 157 | pub fn new(sequence: &Sequence) -> Self { 158 | Self::new_with(sequence.inner.clone()) 159 | } 160 | 161 | /// Returns a new `Some(SequenceHandle)` for a sequence that was defined by [`InSequence`] 162 | /// before. `None` is returned if no [`InSequence`] instance is known in the current 163 | /// context. 164 | #[must_use] 165 | pub fn create_handle() -> Option { 166 | CURRENT_SEQUENCE.with(|cell| { 167 | cell.borrow() 168 | .as_ref() 169 | .map(|inner| Inner::create_handle(inner.clone())) 170 | }) 171 | } 172 | 173 | fn new_with(inner: Arc>) -> Self { 174 | let parent = CURRENT_SEQUENCE.with(|cell| cell.borrow_mut().replace(inner)); 175 | 176 | Self { parent } 177 | } 178 | } 179 | 180 | impl Default for InSequence { 181 | fn default() -> Self { 182 | Self::new_with(Arc::new(Mutex::new(Inner::default()))) 183 | } 184 | } 185 | 186 | impl Drop for InSequence { 187 | fn drop(&mut self) { 188 | CURRENT_SEQUENCE.with(|cell| *cell.borrow_mut() = self.parent.take()); 189 | } 190 | } 191 | 192 | /// Inner state of a sequence. 193 | #[derive(Debug)] 194 | struct Inner { 195 | items: Vec, 196 | current_id: usize, 197 | sequence_id: usize, 198 | } 199 | 200 | #[derive(Default, Debug)] 201 | struct Meta { 202 | is_ready: bool, 203 | description: String, 204 | } 205 | 206 | impl Inner { 207 | fn create_handle(inner: Arc>) -> SequenceHandle { 208 | let (id, sequence_id) = { 209 | let mut inner = inner.lock(); 210 | let id = inner.items.len(); 211 | inner.items.push(Meta::default()); 212 | 213 | (id, inner.sequence_id) 214 | }; 215 | 216 | SequenceHandle { 217 | id, 218 | inner, 219 | sequence_id, 220 | } 221 | } 222 | 223 | fn is_active(&mut self, id: usize) -> bool { 224 | loop { 225 | if self.current_id == id { 226 | return true; 227 | } else if self.current_id < id && self.item(self.current_id).is_ready { 228 | self.current_id += 1; 229 | } else { 230 | return false; 231 | } 232 | } 233 | } 234 | 235 | fn is_done(&mut self, id: usize) -> bool { 236 | self.current_id > id 237 | } 238 | 239 | fn set_ready(&mut self, id: usize) { 240 | self.item_mut(id).is_ready = true; 241 | } 242 | 243 | fn set_done(&mut self, id: usize) { 244 | if self.current_id == id { 245 | self.current_id += 1; 246 | } 247 | } 248 | 249 | fn set_description(&mut self, id: usize, value: String) { 250 | self.item_mut(id).description = value; 251 | } 252 | 253 | fn item(&self, id: usize) -> &Meta { 254 | self.items.get(id).expect("Invalid sequence handle") 255 | } 256 | 257 | fn item_mut(&mut self, id: usize) -> &mut Meta { 258 | self.items.get_mut(id).expect("Invalid sequence handle") 259 | } 260 | } 261 | 262 | impl Default for Inner { 263 | fn default() -> Self { 264 | Self { 265 | sequence_id: SEQUENCE_ID.fetch_add(1, Ordering::Relaxed), 266 | items: vec![Meta { 267 | is_ready: true, 268 | description: "root".into(), 269 | }], 270 | current_id: 0, 271 | } 272 | } 273 | } 274 | 275 | static SEQUENCE_ID: AtomicUsize = AtomicUsize::new(0); 276 | 277 | thread_local! { 278 | static CURRENT_SEQUENCE: RefCell>>> = const { RefCell::new(None) }; 279 | } 280 | -------------------------------------------------------------------------------- /murf/src/times.rs: -------------------------------------------------------------------------------- 1 | //! The [`times`](self) module contains different types and helpers to define 2 | //! how often a call expectation may be called. 3 | 4 | use std::ops::{ 5 | Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive, 6 | }; 7 | use std::sync::atomic::{AtomicUsize, Ordering}; 8 | 9 | /// Type to keep track of the number of calls expected for a specific call expectation. 10 | #[derive(Default, Debug)] 11 | pub struct Times { 12 | /// Number of calls the expectation was already executed. 13 | pub count: AtomicUsize, 14 | 15 | /// Expected number of calls. 16 | pub range: TimesRange, 17 | } 18 | 19 | impl Times { 20 | /// Create a new [`Times`] instance from the passed `range`. 21 | pub fn new>(range: R) -> Self { 22 | Self { 23 | count: AtomicUsize::default(), 24 | range: range.into(), 25 | } 26 | } 27 | 28 | /// Increment the current call count. 29 | pub fn increment(&self) -> usize { 30 | self.count.fetch_add(1, Ordering::Relaxed) 31 | } 32 | 33 | /// Return `true` if lower bound of the range is fulfilled. 34 | pub fn is_ready(&self) -> bool { 35 | match &self.range.lower { 36 | Bound::Unbounded => true, 37 | Bound::Included(x) => *x <= self.count.load(Ordering::Relaxed), 38 | Bound::Excluded(x) => *x < self.count.load(Ordering::Relaxed), 39 | } 40 | } 41 | 42 | /// Return `true` if upper bound of the range is fulfilled. 43 | pub fn is_done(&self) -> bool { 44 | match &self.range.upper { 45 | Bound::Unbounded => false, 46 | Bound::Included(x) => self.count.load(Ordering::Relaxed) >= *x, 47 | Bound::Excluded(x) => self.count.load(Ordering::Relaxed) + 1 >= *x, 48 | } 49 | } 50 | } 51 | 52 | /// Defines the range of expected calls with a lower and a upper limit. 53 | /// 54 | /// Similar to [`RangeBounds`] from the standard library but as struct instead 55 | /// of trait. 56 | #[derive(Debug)] 57 | pub struct TimesRange { 58 | lower: Bound, 59 | upper: Bound, 60 | } 61 | 62 | impl Default for TimesRange { 63 | fn default() -> Self { 64 | Self { 65 | lower: Bound::Unbounded, 66 | upper: Bound::Unbounded, 67 | } 68 | } 69 | } 70 | 71 | impl From for TimesRange { 72 | fn from(value: usize) -> Self { 73 | Self { 74 | lower: Bound::Included(value), 75 | upper: Bound::Included(value), 76 | } 77 | } 78 | } 79 | 80 | macro_rules! impl_from_range_bounds { 81 | ($x:ty) => { 82 | impl From<$x> for TimesRange { 83 | fn from(value: $x) -> Self { 84 | Self { 85 | lower: value.start_bound().cloned(), 86 | upper: value.end_bound().cloned(), 87 | } 88 | } 89 | } 90 | }; 91 | } 92 | 93 | impl_from_range_bounds!(Range); 94 | impl_from_range_bounds!(RangeFrom); 95 | impl_from_range_bounds!(RangeFull); 96 | impl_from_range_bounds!(RangeInclusive); 97 | impl_from_range_bounds!(RangeTo); 98 | impl_from_range_bounds!(RangeToInclusive); 99 | 100 | #[cfg(test)] 101 | mod tests { 102 | use super::Times; 103 | 104 | #[test] 105 | fn number() { 106 | let t = Times::new(1); 107 | assert!(!t.is_ready()); 108 | assert!(!t.is_done()); 109 | t.increment(); 110 | assert!(t.is_ready()); 111 | assert!(t.is_done()); 112 | } 113 | 114 | #[test] 115 | fn range() { 116 | let t = Times::new(1..3); 117 | assert!(!t.is_ready()); 118 | assert!(!t.is_done()); 119 | t.increment(); 120 | assert!(t.is_ready()); 121 | assert!(!t.is_done()); 122 | t.increment(); 123 | assert!(t.is_ready()); 124 | assert!(t.is_done()); 125 | t.increment(); 126 | assert!(t.is_ready()); 127 | assert!(t.is_done()); 128 | } 129 | 130 | #[test] 131 | fn range_from() { 132 | let t = Times::new(2..); 133 | assert!(!t.is_ready()); 134 | assert!(!t.is_done()); 135 | t.increment(); 136 | assert!(!t.is_ready()); 137 | assert!(!t.is_done()); 138 | t.increment(); 139 | assert!(t.is_ready()); 140 | assert!(!t.is_done()); 141 | t.increment(); 142 | assert!(t.is_ready()); 143 | assert!(!t.is_done()); 144 | } 145 | 146 | #[test] 147 | fn range_full() { 148 | let t = Times::new(..); 149 | assert!(t.is_ready()); 150 | assert!(!t.is_done()); 151 | t.increment(); 152 | assert!(t.is_ready()); 153 | assert!(!t.is_done()); 154 | t.increment(); 155 | assert!(t.is_ready()); 156 | assert!(!t.is_done()); 157 | t.increment(); 158 | assert!(t.is_ready()); 159 | assert!(!t.is_done()); 160 | } 161 | 162 | #[test] 163 | fn range_inclusive() { 164 | let t = Times::new(2..=3); 165 | assert!(!t.is_ready()); 166 | assert!(!t.is_done()); 167 | t.increment(); 168 | assert!(!t.is_ready()); 169 | assert!(!t.is_done()); 170 | t.increment(); 171 | assert!(t.is_ready()); 172 | assert!(!t.is_done()); 173 | t.increment(); 174 | assert!(t.is_ready()); 175 | assert!(t.is_done()); 176 | t.increment(); 177 | assert!(t.is_ready()); 178 | assert!(t.is_done()); 179 | } 180 | 181 | #[test] 182 | fn range_to() { 183 | let t = Times::new(..3); 184 | assert!(t.is_ready()); 185 | assert!(!t.is_done()); 186 | t.increment(); 187 | assert!(t.is_ready()); 188 | assert!(!t.is_done()); 189 | t.increment(); 190 | assert!(t.is_ready()); 191 | assert!(t.is_done()); 192 | t.increment(); 193 | assert!(t.is_ready()); 194 | assert!(t.is_done()); 195 | } 196 | 197 | #[test] 198 | fn range_to_inclusive() { 199 | let t = Times::new(..=3); 200 | assert!(t.is_ready()); 201 | assert!(!t.is_done()); 202 | t.increment(); 203 | assert!(t.is_ready()); 204 | assert!(!t.is_done()); 205 | t.increment(); 206 | assert!(t.is_ready()); 207 | assert!(!t.is_done()); 208 | t.increment(); 209 | assert!(t.is_ready()); 210 | assert!(t.is_done()); 211 | t.increment(); 212 | assert!(t.is_ready()); 213 | assert!(t.is_done()); 214 | } 215 | 216 | #[test] 217 | fn range_zero_to_one() { 218 | let t = Times::new(0..=1); 219 | assert!(t.is_ready()); 220 | assert!(!t.is_done()); 221 | t.increment(); 222 | assert!(t.is_ready()); 223 | assert!(t.is_done()); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /murf/src/types/duration.rs: -------------------------------------------------------------------------------- 1 | //! Implements the [`Duration`] type. 2 | 3 | use std::cmp::Ordering; 4 | use std::fmt::{Debug, Display, Formatter, Result as FmtResult}; 5 | use std::ops::{Deref, DerefMut}; 6 | use std::str::FromStr; 7 | use std::time::Duration as StdDuration; 8 | 9 | use parse_duration::{parse, parse::Error}; 10 | 11 | /// Duration type that can be used inside a [`Matcher`](crate::Matcher). 12 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] 13 | pub struct Duration(pub StdDuration); 14 | 15 | macro_rules! impl_from { 16 | (fn $name:ident(value: $type:ty)) => { 17 | #[must_use] 18 | #[allow(missing_docs)] 19 | pub fn $name(value: $type) -> Self { 20 | Self(StdDuration::$name(value)) 21 | } 22 | }; 23 | } 24 | 25 | impl Duration { 26 | impl_from!(fn from_secs(value: u64)); 27 | impl_from!(fn from_millis(value: u64)); 28 | impl_from!(fn from_micros(value: u64)); 29 | impl_from!(fn from_nanos(value: u64)); 30 | impl_from!(fn from_secs_f32(value: f32)); 31 | impl_from!(fn from_secs_f64(value: f64)); 32 | } 33 | 34 | impl From for Duration { 35 | fn from(value: StdDuration) -> Self { 36 | Self(value) 37 | } 38 | } 39 | 40 | impl From for StdDuration { 41 | fn from(value: Duration) -> Self { 42 | value.0 43 | } 44 | } 45 | 46 | impl FromStr for Duration { 47 | type Err = Error; 48 | 49 | fn from_str(s: &str) -> Result { 50 | parse(s).map(Self) 51 | } 52 | } 53 | 54 | impl Display for Duration { 55 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 56 | let secs = self.0.as_secs(); 57 | let nanos = self.0.subsec_nanos(); 58 | 59 | write!(f, "{secs}.{nanos:09}") 60 | } 61 | } 62 | 63 | impl PartialEq for Duration { 64 | fn eq(&self, other: &StdDuration) -> bool { 65 | self.0.eq(other) 66 | } 67 | } 68 | 69 | impl PartialOrd for Duration { 70 | fn partial_cmp(&self, other: &StdDuration) -> Option { 71 | Some(self.0.cmp(other)) 72 | } 73 | } 74 | 75 | impl Deref for Duration { 76 | type Target = StdDuration; 77 | 78 | fn deref(&self) -> &Self::Target { 79 | &self.0 80 | } 81 | } 82 | 83 | impl DerefMut for Duration { 84 | fn deref_mut(&mut self) -> &mut Self::Target { 85 | &mut self.0 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /murf/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | //! The [`types`](self) module defines different default types that can be used 2 | //! as [`Matcher`](crate::Matcher) in a call expectation. 3 | 4 | pub mod duration; 5 | 6 | pub use duration::Duration; 7 | -------------------------------------------------------------------------------- /murf/tests/actions/invoke.rs: -------------------------------------------------------------------------------- 1 | use murf::{expect_method_call, mock}; 2 | 3 | trait Fuu { 4 | #[allow(clippy::mut_from_ref)] 5 | fn fuu(&self, x: usize) -> &mut usize; 6 | } 7 | 8 | mock! { 9 | #[derive(Default)] 10 | pub struct MyStruct; 11 | 12 | impl Fuu for MyStruct { 13 | fn fuu(&self, _x: usize) -> &mut usize; 14 | } 15 | } 16 | 17 | #[test] 18 | fn success() { 19 | let mut i = 5; 20 | let i_ref = &mut i; 21 | 22 | let (handle, mock) = MyStruct::mock_with_handle(); 23 | 24 | expect_method_call!(handle as Fuu, fuu(_)).will_once(move |(_, x): (&_, usize)| { 25 | assert_eq!(x, 5); 26 | 27 | *i_ref = x; 28 | 29 | i_ref 30 | }); 31 | 32 | mock.fuu(5); 33 | 34 | drop(handle); 35 | drop(mock); 36 | 37 | assert_eq!(5, i); 38 | } 39 | -------------------------------------------------------------------------------- /murf/tests/actions/mod.rs: -------------------------------------------------------------------------------- 1 | mod invoke; 2 | mod return_once; 3 | mod return_pointee; 4 | -------------------------------------------------------------------------------- /murf/tests/actions/return_once.rs: -------------------------------------------------------------------------------- 1 | use murf::{action::Return, expect_method_call, mock}; 2 | 3 | #[derive(Debug, Eq, PartialEq)] 4 | pub struct Data(usize); 5 | 6 | trait Fuu { 7 | fn fuu(&self) -> Data; 8 | } 9 | 10 | mock! { 11 | #[derive(Default)] 12 | pub struct MyStruct; 13 | 14 | impl Fuu for MyStruct { 15 | fn fuu(&self) -> Data; 16 | } 17 | } 18 | 19 | #[test] 20 | fn success() { 21 | let (handle, mock) = MyStruct::mock_with_handle(); 22 | 23 | expect_method_call!(handle as Fuu, fuu()).will_once(Return(Data(1))); 24 | expect_method_call!(handle as Fuu, fuu()).will_once(Return(Data(2))); 25 | 26 | assert_eq!(Data(1), mock.fuu()); 27 | assert_eq!(Data(2), mock.fuu()); 28 | } 29 | -------------------------------------------------------------------------------- /murf/tests/actions/return_pointee.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | use murf::{action::ReturnPointee, expect_method_call, mock}; 4 | 5 | trait Fuu { 6 | fn fuu(&self) -> usize; 7 | } 8 | 9 | mock! { 10 | #[derive(Default)] 11 | pub struct MyStruct; 12 | 13 | impl Fuu for MyStruct { 14 | fn fuu(&self) -> usize; 15 | } 16 | } 17 | 18 | #[test] 19 | fn success() { 20 | let val = Rc::new(RefCell::new(5usize)); 21 | 22 | let (handle, mock) = MyStruct::mock_with_handle(); 23 | 24 | expect_method_call!(handle as Fuu, fuu()).will_once(ReturnPointee(val.clone())); 25 | expect_method_call!(handle as Fuu, fuu()).will_once(ReturnPointee(val.clone())); 26 | 27 | assert_eq!(5, mock.fuu()); 28 | 29 | *val.borrow_mut() = 10; 30 | 31 | assert_eq!(10, mock.fuu()); 32 | 33 | drop(handle); 34 | drop(mock); 35 | } 36 | -------------------------------------------------------------------------------- /murf/tests/interface/argument_with_default_lifetime.rs: -------------------------------------------------------------------------------- 1 | use std::pin::Pin; 2 | use std::task::{Context, Poll}; 3 | 4 | use futures::task::noop_waker_ref; 5 | use futures::Stream; 6 | 7 | use murf::{action::Return, expect_method_call, mock}; 8 | 9 | mock! { 10 | #[derive(Default)] 11 | pub struct MyStruct; 12 | 13 | impl Stream for MyStruct { 14 | type Item = usize; 15 | 16 | fn poll_next( 17 | self: Pin<&mut Self>, 18 | _cx: &mut Context<'_> 19 | ) -> Poll>; 20 | } 21 | } 22 | 23 | #[test] 24 | fn success() { 25 | let (handle, mut mock) = MyStruct::mock_with_handle(); 26 | 27 | expect_method_call!(handle as Stream, poll_next(_)).will_once(Return(Poll::Ready(None))); 28 | 29 | let mut cx = Context::from_waker(noop_waker_ref()); 30 | assert_eq!(Poll::Ready(None), Pin::new(&mut mock).poll_next(&mut cx)); 31 | } 32 | -------------------------------------------------------------------------------- /murf/tests/interface/argument_with_lifetime.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use murf::{expect_method_call, matcher::Closure, mock}; 4 | 5 | pub struct Event<'a>(PhantomData<&'a ()>); 6 | 7 | trait Fuu { 8 | fn fuu(&self, x: E); 9 | } 10 | 11 | mock! { 12 | #[derive(Default)] 13 | pub struct Handler; 14 | 15 | impl<'a> Fuu> for Handler { 16 | fn fuu(&self, _x: Event<'a>); 17 | } 18 | } 19 | 20 | #[test] 21 | fn success() { 22 | let (handle, mock) = Handler::mock_with_handle(); 23 | 24 | expect_method_call!(handle as Fuu, fuu(Closure(|_: &Event<'_>| true))); 25 | 26 | mock.fuu(Event(PhantomData)); 27 | } 28 | -------------------------------------------------------------------------------- /murf/tests/interface/associated_functions.rs: -------------------------------------------------------------------------------- 1 | use murf::{action::Return, expect_call, matcher::eq, mock}; 2 | 3 | trait Fuu { 4 | fn fuu(x: usize) -> usize; 5 | } 6 | 7 | mock! { 8 | #[derive(Default)] 9 | pub struct MyStruct; 10 | 11 | impl Fuu for MyStruct { 12 | fn fuu(_x: usize) -> usize; 13 | } 14 | } 15 | 16 | #[test] 17 | fn success() { 18 | let (handle, _mock) = MyStruct::mock_with_handle(); 19 | 20 | expect_call!(handle as Fuu, fuu(eq(4))).will_once(Return(4)); 21 | 22 | assert_eq!(4, MyStructMock::fuu(4)); 23 | } 24 | -------------------------------------------------------------------------------- /murf/tests/interface/associated_type_trait.rs: -------------------------------------------------------------------------------- 1 | use murf::{action::Return, expect_method_call, mock}; 2 | 3 | mock! { 4 | #[derive(Default)] 5 | pub struct MyStruct; 6 | 7 | impl Iterator for MyStruct { 8 | type Item = usize; 9 | 10 | fn next(&mut self) -> Option; 11 | } 12 | } 13 | 14 | struct NewIterator { 15 | inner: T, 16 | } 17 | 18 | impl NewIterator { 19 | fn new(inner: T) -> Self { 20 | Self { inner } 21 | } 22 | 23 | fn next(&mut self) -> Option { 24 | self.inner.next() 25 | } 26 | } 27 | 28 | #[test] 29 | fn success() { 30 | let (handle, mock) = MyStruct::mock_with_handle(); 31 | 32 | let mut iter = NewIterator::new(mock); 33 | 34 | expect_method_call!(handle as Iterator, next()).will_once(Return(Some(2))); 35 | 36 | assert_eq!(Some(2), iter.next()); 37 | } 38 | -------------------------------------------------------------------------------- /murf/tests/interface/associated_type_trait_with_lifetime.rs: -------------------------------------------------------------------------------- 1 | use std::slice::Iter; 2 | 3 | use murf::{action::Return, expect_method_call, mock}; 4 | 5 | trait Values { 6 | type Iter<'x>: Iterator 7 | where 8 | T: 'x, 9 | Self: 'x; 10 | 11 | fn values(&self) -> Self::Iter<'_>; 12 | } 13 | 14 | mock! { 15 | #[derive(Default)] 16 | pub struct MyStruct; 17 | 18 | impl Values for MyStruct { 19 | type Iter<'x> = Iter<'x, usize> 20 | where 21 | Self: 'x; 22 | 23 | fn values(&self) -> Iter<'_, usize>; 24 | } 25 | } 26 | 27 | #[test] 28 | fn success() { 29 | let values: Vec = vec![1, 2, 3, 4]; 30 | 31 | let (handle, mock) = MyStruct::mock_with_handle::<'_>(); 32 | 33 | expect_method_call!(handle as Values, values()).will_once(Return(values.iter())); 34 | 35 | let values = mock.values().cloned().collect::>(); 36 | 37 | assert_eq!(values, [1, 2, 3, 4]); 38 | } 39 | -------------------------------------------------------------------------------- /murf/tests/interface/clonable_mock.rs: -------------------------------------------------------------------------------- 1 | use murf::{expect_method_call, matcher::Eq, mock, InSequence}; 2 | 3 | trait Fuu { 4 | fn fuu(&self, arg: usize); 5 | } 6 | 7 | mock! { 8 | #[derive(Default, Clone)] 9 | pub struct MyStruct; 10 | 11 | impl Fuu for MyStruct { 12 | fn fuu(&self, _arg: usize); 13 | } 14 | } 15 | 16 | #[test] 17 | fn success() { 18 | let (handle, mock1) = MyStruct::mock_with_handle(); 19 | 20 | let _seq = InSequence::default(); 21 | expect_method_call!(handle as Fuu, fuu(Eq(1))).times(1); 22 | expect_method_call!(handle as Fuu, fuu(Eq(2))).times(1); 23 | 24 | let mock2 = mock1.clone(); 25 | 26 | mock1.fuu(1); 27 | mock2.fuu(2); 28 | } 29 | -------------------------------------------------------------------------------- /murf/tests/interface/constructor_with_args.rs: -------------------------------------------------------------------------------- 1 | use murf::{action::Return, expect_call, expect_method_call, matcher::eq, mock}; 2 | 3 | trait Fuu: Sized { 4 | fn new(x: usize) -> Result; 5 | 6 | fn fuu(&self) -> usize; 7 | } 8 | 9 | mock! { 10 | #[derive(Default, Send, Sync)] 11 | pub struct MyStruct; 12 | 13 | impl Fuu for MyStruct { 14 | fn new(x: usize) -> Result; 15 | 16 | fn fuu(&self) -> usize; 17 | } 18 | } 19 | 20 | #[test] 21 | fn success() { 22 | let static_handle = MyStructHandle::new(); 23 | 24 | let (mock_handle, mock) = MyStruct::mock_with_handle(); 25 | 26 | expect_call!(static_handle as Fuu, new(eq(4))).will_once(Return(Ok(mock))); 27 | expect_method_call!(mock_handle as Fuu, fuu()).will_once(Return(5)); 28 | 29 | let my_struct = ::new(4).unwrap(); 30 | assert_eq!(5, my_struct.fuu()); 31 | } 32 | -------------------------------------------------------------------------------- /murf/tests/interface/expect_call.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use murf::{expect_method_call, mock}; 4 | 5 | trait Fuu { 6 | fn fuu(&self); 7 | } 8 | 9 | mock! { 10 | #[derive(Default)] 11 | pub struct MyStruct; 12 | 13 | impl Fuu for MyStruct { 14 | fn fuu(&self); 15 | } 16 | } 17 | 18 | #[test] 19 | fn success() { 20 | let (handle, _mock) = MyStruct::mock_with_handle(); 21 | 22 | expect_method_call!(handle as Fuu, fuu()).times(0); 23 | 24 | let tuple = (handle, 0); 25 | 26 | expect_method_call!(tuple.0 as Fuu, fuu()).times(0); 27 | } 28 | -------------------------------------------------------------------------------- /murf/tests/interface/expect_call_with_const_generics.rs: -------------------------------------------------------------------------------- 1 | use murf::{expect_method_call, mock}; 2 | 3 | pub trait Fuu { 4 | fn fuu(&self); 5 | } 6 | 7 | mock! { 8 | #[derive(Default, Debug)] 9 | pub struct MockedFuu; 10 | 11 | impl Fuu for MockedFuu { 12 | fn fuu(&self); 13 | } 14 | } 15 | 16 | #[test] 17 | fn test() { 18 | let mock = MockedFuu::mock(); 19 | 20 | expect_method_call!(mock as Fuu, fuu::<1024>()); 21 | 22 | mock.fuu::<1024>(); 23 | } 24 | -------------------------------------------------------------------------------- /murf/tests/interface/expect_call_with_generics.rs: -------------------------------------------------------------------------------- 1 | use murf::{action::Return, expect_method_call, matcher::eq, mock}; 2 | 3 | pub trait Fuu { 4 | fn fuu(&self, value: X) -> T; 5 | } 6 | 7 | mock! { 8 | #[derive(Default, Debug)] 9 | pub struct MockedFuu; 10 | 11 | impl Fuu for MockedFuu { 12 | fn fuu(&self, value: X) -> T; 13 | } 14 | } 15 | 16 | #[test] 17 | fn test() { 18 | let mock = MockedFuu::mock(); 19 | 20 | expect_method_call!(mock as Fuu, fuu(eq(123u8))).will_once(Return(312usize)); 21 | expect_method_call!(mock as Fuu, fuu::<_>(eq(123u8))).will_once(Return(312usize)); 22 | 23 | assert_eq!(312usize, mock.fuu(123u8)); 24 | assert_eq!(312usize, mock.fuu(123u8)); 25 | } 26 | -------------------------------------------------------------------------------- /murf/tests/interface/exsiting_type.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::marker::PhantomData; 3 | 4 | use murf::{action::Return, expect_method_call, mock}; 5 | 6 | trait Fuu { 7 | fn fuu(&self) -> usize; 8 | } 9 | 10 | #[derive(Default)] 11 | pub struct MyStruct(PhantomData); 12 | 13 | impl Fuu for MyStruct { 14 | fn fuu(&self) -> usize { 15 | 6 16 | } 17 | } 18 | 19 | mock! { 20 | impl Fuu for MyStruct { 21 | fn fuu(&self) -> usize; 22 | } 23 | } 24 | 25 | #[test] 26 | fn success() { 27 | let (handle, mock) = MyStruct::::mock_with_handle(); 28 | 29 | expect_method_call!(handle as Fuu, fuu()).times(1); 30 | expect_method_call!(handle as Fuu, fuu()).will_once(Return(4)); 31 | 32 | assert_eq!(6, mock.fuu()); 33 | assert_eq!(4, mock.fuu()); 34 | } 35 | -------------------------------------------------------------------------------- /murf/tests/interface/generic_associated_type_trait.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | use std::marker::PhantomData; 3 | 4 | use murf::{action::Return, expect_method_call, mock}; 5 | 6 | mock! { 7 | #[derive(Default)] 8 | pub struct MyStruct<'a, T>(PhantomData<&'a T>) 9 | where 10 | T: Default + Debug; 11 | 12 | impl<'a, T> Iterator for MyStruct<'a, T> 13 | where 14 | T: Default + Debug, 15 | { 16 | type Item = &'a T; 17 | 18 | fn next(&mut self) -> Option<&'a T>; 19 | } 20 | } 21 | 22 | struct NewIterator { 23 | inner: T, 24 | } 25 | 26 | impl NewIterator { 27 | fn new(inner: T) -> Self { 28 | Self { inner } 29 | } 30 | 31 | fn next(&mut self) -> Option { 32 | self.inner.next() 33 | } 34 | } 35 | 36 | #[test] 37 | fn success() { 38 | let (handle, mock) = MyStruct::::mock_with_handle(); 39 | 40 | let mut iter = NewIterator::new(mock); 41 | 42 | expect_method_call!(handle as Iterator, next()).will_once(Return(Some(&2))); 43 | 44 | assert_eq!(Some(&2), iter.next()); 45 | } 46 | -------------------------------------------------------------------------------- /murf/tests/interface/generic_trait.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter, Result as FmtResult}; 2 | 3 | use murf::{expect_method_call, matcher::eq, mock}; 4 | 5 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 6 | struct Wrapper<'a, T>(&'a T); 7 | 8 | impl<'a, T: Display> Display for Wrapper<'a, T> { 9 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 10 | write!(f, "Wrapper({})", &self.0) 11 | } 12 | } 13 | 14 | trait Fuu { 15 | fn fuu(&self, arg: T); 16 | } 17 | 18 | mock! { 19 | #[derive(Default)] 20 | pub struct MyStruct; 21 | 22 | impl Fuu for MyStruct { 23 | fn fuu(&self, _arg: T); 24 | } 25 | } 26 | 27 | #[test] 28 | fn success() { 29 | let fuu = 123usize; 30 | let fuu = Wrapper(&fuu); 31 | let (handle, mock) = MyStruct::mock_with_handle(); 32 | 33 | expect_method_call!(handle as Fuu, fuu(eq(fuu))); 34 | 35 | mock.fuu(Wrapper(&123usize)); 36 | } 37 | -------------------------------------------------------------------------------- /murf/tests/interface/in_sequence.rs: -------------------------------------------------------------------------------- 1 | use murf::{expect_method_call, matcher::eq, mock, InSequence}; 2 | 3 | trait Fuu { 4 | fn fuu(&self, x: usize); 5 | } 6 | 7 | mock! { 8 | #[derive(Default)] 9 | pub struct MyStruct; 10 | 11 | impl Fuu for MyStruct { 12 | fn fuu(&self, _x: usize); 13 | } 14 | } 15 | 16 | #[test] 17 | fn success() { 18 | let (handle, mock) = MyStruct::mock_with_handle(); 19 | 20 | let _seq = InSequence::default(); 21 | expect_method_call!(handle as Fuu, fuu(eq(1))); 22 | expect_method_call!(handle as Fuu, fuu(eq(2))); 23 | 24 | mock.fuu(1); 25 | mock.fuu(2); 26 | } 27 | 28 | #[test] 29 | fn remove_sequences() { 30 | let (handle, mock) = MyStruct::mock_with_handle(); 31 | 32 | let _seq = InSequence::default(); 33 | expect_method_call!(handle as Fuu, fuu(eq(1))); 34 | expect_method_call!(handle as Fuu, fuu(eq(2))).no_sequences(); 35 | expect_method_call!(handle as Fuu, fuu(eq(3))); 36 | 37 | mock.fuu(1); 38 | mock.fuu(3); 39 | 40 | mock.fuu(2); 41 | } 42 | 43 | #[test] 44 | #[should_panic] 45 | fn failure() { 46 | let (handle, mock) = MyStruct::mock_with_handle(); 47 | 48 | let _seq = InSequence::default(); 49 | expect_method_call!(handle as Fuu, fuu(eq(1))); 50 | expect_method_call!(handle as Fuu, fuu(eq(2))); 51 | 52 | mock.fuu(2); 53 | mock.fuu(1); 54 | } 55 | -------------------------------------------------------------------------------- /murf/tests/interface/local_context.rs: -------------------------------------------------------------------------------- 1 | use murf::{action::Return, expect_call, matcher::eq, mock, LocalContext}; 2 | 3 | trait Fuu { 4 | fn fuu(x: usize) -> usize; 5 | } 6 | 7 | mock! { 8 | #[derive(Default)] 9 | pub struct MyStruct; 10 | 11 | impl Fuu for MyStruct { 12 | fn fuu(_x: usize) -> usize; 13 | } 14 | } 15 | 16 | #[test] 17 | fn success() { 18 | let local_context = LocalContext::new(); 19 | 20 | let (handle, _mock) = MyStruct::mock_with_handle(); 21 | 22 | expect_call!(handle as Fuu, fuu(eq(4))).will_once(Return(4)); 23 | 24 | let type_id = *mock_impl_my_struct::mock_trait_fuu_method_fuu::TYPE_ID; 25 | let expectations = LocalContext::current() 26 | .borrow_mut() 27 | .as_ref() 28 | .unwrap() 29 | .expectations(type_id) 30 | .count(); 31 | assert_eq!(expectations, 1); 32 | 33 | assert_eq!(4, MyStructMock::fuu(4)); 34 | 35 | drop(local_context); 36 | } 37 | -------------------------------------------------------------------------------- /murf/tests/interface/mock_lifetime.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use murf::{action::Return, mock}; 4 | use murf_macros::expect_method_call; 5 | 6 | trait Fuu { 7 | fn fuu(&self); 8 | } 9 | 10 | trait Bar { 11 | type Fuu: Fuu; 12 | 13 | fn bar(&self) -> &Self::Fuu; 14 | } 15 | 16 | mock! { 17 | #[derive(Default)] 18 | pub struct MockedFuu; 19 | 20 | impl Fuu for MockedFuu { 21 | fn fuu(&self); 22 | } 23 | } 24 | 25 | mock! { 26 | #[derive(Default)] 27 | pub struct MockedBar<'mock, 'fuu> 28 | where 29 | 'fuu: 'mock, 30 | { 31 | mock_lt: PhantomData<&'mock ()>, 32 | fuu_lt: PhantomData<&'fuu ()>, 33 | } 34 | 35 | impl<'mock, 'fuu> Bar for MockedBar<'mock, 'fuu> 36 | where 37 | 'fuu: 'mock, 38 | { 39 | type Fuu = MockedFuuMock<'fuu>; 40 | 41 | fn bar(&self) -> &MockedFuuMock<'fuu>; 42 | } 43 | } 44 | 45 | struct Test<'mock, 'fuu> 46 | where 47 | 'fuu: 'mock, 48 | { 49 | bar: MockedBarHandle<'mock, 'fuu>, 50 | bar_mock: MockedBarMock<'mock, 'fuu>, 51 | } 52 | 53 | impl<'mock, 'fuu> Test<'mock, 'fuu> 54 | where 55 | 'fuu: 'mock, 56 | { 57 | fn new() -> Test<'mock, 'fuu> { 58 | let (bar, bar_mock) = MockedBar::mock_with_handle(); 59 | 60 | Test { bar, bar_mock } 61 | } 62 | } 63 | 64 | #[test] 65 | fn test() { 66 | let (fuu, fuu_mock) = MockedFuu::mock_with_handle(); 67 | let Test { bar, bar_mock } = Test::new(); 68 | 69 | expect_method_call!(bar as Bar, bar()).will_once(Return(&fuu_mock)); 70 | expect_method_call!(fuu as Fuu, fuu()); 71 | 72 | bar_mock.bar().fuu(); 73 | } 74 | -------------------------------------------------------------------------------- /murf/tests/interface/mod.rs: -------------------------------------------------------------------------------- 1 | mod argument_with_default_lifetime; 2 | mod argument_with_lifetime; 3 | mod associated_functions; 4 | mod associated_type_trait; 5 | mod associated_type_trait_with_lifetime; 6 | mod clonable_mock; 7 | mod constructor_with_args; 8 | mod expect_call; 9 | mod expect_call_with_const_generics; 10 | mod expect_call_with_generics; 11 | mod exsiting_type; 12 | mod generic_associated_type_trait; 13 | mod generic_trait; 14 | mod in_sequence; 15 | mod local_context; 16 | mod mock_lifetime; 17 | mod no_default; 18 | mod reference_argument; 19 | mod return_self_type; 20 | mod self_arc; 21 | mod sequence; 22 | mod simple_trait; 23 | mod times; 24 | mod trait_bound_with_self_type; 25 | -------------------------------------------------------------------------------- /murf/tests/interface/no_default.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | use futures::{task::noop_waker_ref, Stream}; 6 | 7 | use murf::{action::Return, expect_method_call, mock}; 8 | 9 | mock! { 10 | pub struct MyStruct(PhantomData); 11 | 12 | impl Stream for MyStruct { 13 | type Item = T; 14 | 15 | fn poll_next( 16 | self: Pin<&mut Self>, 17 | _cx: &mut Context<'_> 18 | ) -> Poll>; 19 | } 20 | } 21 | 22 | #[test] 23 | fn success() { 24 | let (handle, mut mock) = MyStruct(PhantomData::).into_mock().mock_split(); 25 | 26 | expect_method_call!(handle as Stream, poll_next(_)).will_once(Return(Poll::Ready(None))); 27 | 28 | let mut cx = Context::from_waker(noop_waker_ref()); 29 | assert_eq!(Poll::Ready(None), Pin::new(&mut mock).poll_next(&mut cx)); 30 | } 31 | -------------------------------------------------------------------------------- /murf/tests/interface/reference_argument.rs: -------------------------------------------------------------------------------- 1 | use murf::{action::Return, expect_method_call, matcher::eq, mock}; 2 | 3 | trait Fuu { 4 | fn fuu(&self, x: &usize) -> &usize; 5 | } 6 | 7 | mock! { 8 | #[derive(Default)] 9 | pub struct MyStruct; 10 | 11 | impl Fuu for MyStruct { 12 | fn fuu(&self, _x: &usize) -> &usize; 13 | } 14 | } 15 | 16 | #[test] 17 | fn success() { 18 | let (handle, mock) = MyStruct::mock_with_handle(); 19 | 20 | expect_method_call!(handle as Fuu, fuu(eq(&6))).will_once(Return(&4)); 21 | 22 | assert_eq!(&4, mock.fuu(&6)); 23 | } 24 | -------------------------------------------------------------------------------- /murf/tests/interface/return_self_type.rs: -------------------------------------------------------------------------------- 1 | use murf::{expect_method_call, mock}; 2 | 3 | #[derive(Default, Clone)] 4 | pub struct MyStruct; 5 | 6 | mock! { 7 | impl Clone for MyStruct { 8 | fn clone(&self) -> Self; 9 | } 10 | } 11 | 12 | #[test] 13 | fn success() { 14 | let (handle, mock) = MyStruct::mock_with_handle(); 15 | 16 | expect_method_call!(handle as Clone, clone()); 17 | 18 | let _ = mock.clone(); 19 | } 20 | -------------------------------------------------------------------------------- /murf/tests/interface/self_arc.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use std::task::{Wake, Waker}; 3 | 4 | use murf::{expect_method_call, mock}; 5 | 6 | mock! { 7 | #[derive(Default, Clone, Send, Sync)] 8 | pub struct Wakeable; 9 | 10 | impl Wake for Wakeable { 11 | fn wake(self: Arc); 12 | fn wake_by_ref(self: &Arc); 13 | } 14 | } 15 | 16 | impl WakeableMock<'static> { 17 | pub fn into_waker(self) -> Waker { 18 | Waker::from(Arc::new(self)) 19 | } 20 | } 21 | 22 | #[test] 23 | fn success() { 24 | let (wake_handle, wake_mock) = Wakeable::mock_with_handle(); 25 | let waker = wake_mock.into_waker(); 26 | 27 | expect_method_call!(wake_handle as Wake, wake_by_ref()); 28 | waker.wake_by_ref(); 29 | 30 | expect_method_call!(wake_handle as Wake, wake()); 31 | waker.wake(); 32 | } 33 | -------------------------------------------------------------------------------- /murf/tests/interface/sequence.rs: -------------------------------------------------------------------------------- 1 | use murf::{expect_method_call, matcher::eq, mock, Sequence}; 2 | 3 | trait Fuu { 4 | fn fuu(&self, x: usize); 5 | } 6 | 7 | mock! { 8 | #[derive(Default)] 9 | pub struct MyStruct; 10 | 11 | impl Fuu for MyStruct { 12 | fn fuu(&self, _x: usize); 13 | } 14 | } 15 | 16 | #[test] 17 | fn success() { 18 | let seq = Sequence::default(); 19 | let (handle, mock) = MyStruct::mock_with_handle(); 20 | 21 | expect_method_call!(handle as Fuu, fuu(eq(1))).in_sequence(&seq); 22 | expect_method_call!(handle as Fuu, fuu(eq(2))).in_sequence(&seq); 23 | 24 | mock.fuu(1); 25 | mock.fuu(2); 26 | } 27 | 28 | #[test] 29 | #[should_panic] 30 | fn failure() { 31 | let seq = Sequence::default(); 32 | let (handle, mock) = MyStruct::mock_with_handle(); 33 | 34 | expect_method_call!(handle as Fuu, fuu(eq(1))).in_sequence(&seq); 35 | expect_method_call!(handle as Fuu, fuu(eq(2))).in_sequence(&seq); 36 | 37 | mock.fuu(2); 38 | mock.fuu(1); 39 | } 40 | 41 | #[test] 42 | fn multi_sequence() { 43 | let seq0 = Sequence::default(); 44 | let seq1 = Sequence::default(); 45 | 46 | let (handle, mock) = MyStruct::mock_with_handle(); 47 | 48 | expect_method_call!(handle as Fuu, fuu(eq(1))).add_sequence(&seq0); 49 | expect_method_call!(handle as Fuu, fuu(eq(2))).add_sequence(&seq1); 50 | expect_method_call!(handle as Fuu, fuu(eq(3))) 51 | .add_sequence(&seq0) 52 | .add_sequence(&seq1); 53 | mock.fuu(1); 54 | mock.fuu(2); 55 | mock.fuu(3); 56 | handle.checkpoint(); 57 | 58 | expect_method_call!(handle as Fuu, fuu(eq(1))).add_sequence(&seq0); 59 | expect_method_call!(handle as Fuu, fuu(eq(2))).add_sequence(&seq1); 60 | expect_method_call!(handle as Fuu, fuu(eq(3))) 61 | .add_sequence(&seq0) 62 | .add_sequence(&seq1); 63 | mock.fuu(2); 64 | mock.fuu(1); 65 | mock.fuu(3); 66 | handle.checkpoint(); 67 | } 68 | -------------------------------------------------------------------------------- /murf/tests/interface/simple_trait.rs: -------------------------------------------------------------------------------- 1 | use murf::{action::Return, expect_method_call, matcher::eq, mock}; 2 | 3 | trait Fuu { 4 | fn fuu(&self, x: usize) -> usize; 5 | } 6 | 7 | mock! { 8 | #[derive(Default)] 9 | pub struct MyStruct; 10 | 11 | impl Fuu for MyStruct { 12 | fn fuu(&self, _x: usize) -> usize; 13 | } 14 | } 15 | 16 | struct Service { 17 | fuu: T, 18 | } 19 | 20 | impl Service { 21 | fn new(fuu: T) -> Self { 22 | Self { fuu } 23 | } 24 | 25 | fn exec(&self) -> usize { 26 | self.fuu.fuu(4) 27 | } 28 | } 29 | 30 | #[test] 31 | fn success() { 32 | let (handle, mock) = MyStruct::mock_with_handle(); 33 | 34 | let service = Service::new(mock); 35 | 36 | expect_method_call!(handle as Fuu, fuu(eq(4))).will_once(Return(4)); 37 | 38 | assert_eq!(4, service.exec()); 39 | } 40 | 41 | #[test] 42 | #[should_panic] 43 | fn failure() { 44 | let (handle, mock) = MyStruct::mock_with_handle(); 45 | 46 | let service = Service::new(mock); 47 | 48 | expect_method_call!(handle as Fuu, fuu(_)).will_once(Return(4)); 49 | 50 | drop(service); 51 | } 52 | -------------------------------------------------------------------------------- /murf/tests/interface/times.rs: -------------------------------------------------------------------------------- 1 | use murf::{expect_method_call, mock, InSequence}; 2 | 3 | trait Fuu { 4 | fn fuu(&self); 5 | } 6 | 7 | trait Bar { 8 | fn bar(&self); 9 | } 10 | 11 | mock! { 12 | #[derive(Default)] 13 | pub struct MockedFuu; 14 | 15 | impl Fuu for MockedFuu { 16 | fn fuu(&self); 17 | } 18 | } 19 | 20 | mock! { 21 | #[derive(Default)] 22 | pub struct MockedBar; 23 | 24 | impl Bar for MockedBar { 25 | fn bar(&self); 26 | } 27 | } 28 | 29 | #[test] 30 | fn success() { 31 | let (fuu, fuu_mock) = MockedFuu::mock_with_handle(); 32 | 33 | expect_method_call!(fuu as Fuu, fuu()).times(1); 34 | fuu_mock.fuu(); 35 | fuu.checkpoint(); 36 | 37 | expect_method_call!(fuu as Fuu, fuu()).times(1..4); 38 | fuu_mock.fuu(); 39 | fuu_mock.fuu(); 40 | fuu_mock.fuu(); 41 | fuu.checkpoint(); 42 | 43 | expect_method_call!(fuu as Fuu, fuu()).times(1..=3); 44 | fuu_mock.fuu(); 45 | fuu_mock.fuu(); 46 | fuu_mock.fuu(); 47 | fuu.checkpoint(); 48 | 49 | expect_method_call!(fuu as Fuu, fuu()).times(2..); 50 | fuu_mock.fuu(); 51 | fuu_mock.fuu(); 52 | fuu.checkpoint(); 53 | 54 | expect_method_call!(fuu as Fuu, fuu()).times(..2); 55 | fuu_mock.fuu(); 56 | fuu.checkpoint(); 57 | 58 | expect_method_call!(fuu as Fuu, fuu()).times(..=2); 59 | fuu_mock.fuu(); 60 | fuu_mock.fuu(); 61 | fuu.checkpoint(); 62 | } 63 | 64 | #[test] 65 | fn zero_or_one() { 66 | let _sequence = InSequence::default(); 67 | 68 | { 69 | let (fuu, fuu_mock) = MockedFuu::mock_with_handle(); 70 | let (bar, bar_mock) = MockedBar::mock_with_handle(); 71 | 72 | expect_method_call!(fuu as Fuu, fuu()).times(1); 73 | expect_method_call!(bar as Bar, bar()).times(0..=1); 74 | expect_method_call!(fuu as Fuu, fuu()).times(1); 75 | 76 | fuu_mock.fuu(); 77 | bar_mock.bar(); 78 | fuu_mock.fuu(); 79 | } 80 | 81 | { 82 | let (fuu, fuu_mock) = MockedFuu::mock_with_handle(); 83 | let (bar, _bar_mock) = MockedBar::mock_with_handle(); 84 | 85 | expect_method_call!(fuu as Fuu, fuu()).times(1); 86 | expect_method_call!(bar as Bar, bar()).times(0..=1); 87 | expect_method_call!(fuu as Fuu, fuu()).times(1); 88 | 89 | fuu_mock.fuu(); 90 | fuu_mock.fuu(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /murf/tests/interface/trait_bound_with_self_type.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use murf_macros::mock; 4 | 5 | pub trait Spawner { 6 | fn spawn(task: T); 7 | } 8 | 9 | pub trait Spawnable { 10 | fn spawn(self, spawner: T); 11 | } 12 | 13 | mock! { 14 | pub struct MockedTask; 15 | 16 | impl Spawnable for MockedTask 17 | where 18 | T: Spawner 19 | { 20 | #[murf(no_default_impl)] 21 | fn spawn(self, spawner: T); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /murf/tests/matcher/deref.rs: -------------------------------------------------------------------------------- 1 | use murf::{ 2 | expect_method_call, 3 | matcher::{deref, eq}, 4 | mock, 5 | }; 6 | 7 | trait Fuu { 8 | fn fuu(&self, x: &usize); 9 | } 10 | 11 | mock! { 12 | #[derive(Default)] 13 | pub struct MyStruct; 14 | 15 | impl Fuu for MyStruct { 16 | fn fuu(&self, _x: &usize); 17 | } 18 | } 19 | 20 | #[test] 21 | fn success() { 22 | let (handle, mock) = MyStruct::mock_with_handle(); 23 | 24 | expect_method_call!(handle as Fuu, fuu(deref(eq(4)))); 25 | 26 | mock.fuu(&4); 27 | } 28 | -------------------------------------------------------------------------------- /murf/tests/matcher/mod.rs: -------------------------------------------------------------------------------- 1 | mod deref; 2 | mod multi_args; 3 | mod range; 4 | -------------------------------------------------------------------------------- /murf/tests/matcher/multi_args.rs: -------------------------------------------------------------------------------- 1 | use murf::{expect_method_call, matcher::eq, mock}; 2 | 3 | trait Fuu { 4 | fn fuu(&self, x: usize, y: usize, z: usize); 5 | } 6 | 7 | mock! { 8 | #[derive(Default)] 9 | pub struct MyStruct; 10 | 11 | impl Fuu for MyStruct { 12 | fn fuu(&self, _x: usize, _y: usize, _z: usize); 13 | } 14 | } 15 | 16 | #[test] 17 | fn success() { 18 | let (handle, mock) = MyStruct::mock_with_handle(); 19 | 20 | expect_method_call!(handle as Fuu, fuu(eq(4), eq(4), eq(4))); 21 | 22 | mock.fuu(4, 4, 4); 23 | } 24 | -------------------------------------------------------------------------------- /murf/tests/matcher/range.rs: -------------------------------------------------------------------------------- 1 | use murf::{expect_method_call, matcher::range, mock}; 2 | 3 | trait Fuu { 4 | fn fuu(&self, x: usize); 5 | } 6 | 7 | mock! { 8 | #[derive(Default)] 9 | pub struct MyStruct; 10 | 11 | impl Fuu for MyStruct { 12 | fn fuu(&self, _x: usize); 13 | } 14 | } 15 | 16 | #[test] 17 | fn success() { 18 | let mock = MyStructMock::default(); 19 | 20 | expect_method_call!(mock as Fuu, fuu(range(4..=6))); 21 | 22 | mock.fuu(5); 23 | } 24 | 25 | #[test] 26 | #[should_panic] 27 | fn failure() { 28 | let mock = MyStruct::mock(); 29 | 30 | expect_method_call!(mock as Fuu, fuu(range(4..=6))); 31 | 32 | mock.fuu(7); 33 | } 34 | -------------------------------------------------------------------------------- /murf/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod actions; 2 | mod interface; 3 | mod matcher; 4 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | components = [ 3 | "rls", 4 | "rust-src", 5 | "rustfmt", 6 | "cargo", 7 | "clippy", 8 | ] 9 | --------------------------------------------------------------------------------