Ejemplo n.º 1
0
def test_decode(mock_dictionary_byte_hash: MagicMock):
    mock_dictionary_byte_hash.return_value = 120
    # We're mocking the database hash to avoid breaking tests every single time we change the database

    # This test should break whenever we change how permalinks are created
    # When this happens, we must bump the permalink version and change the tests
    encoded = "gAAAfReLCAAC4wAAAOaANg=="

    expected = Permalink(
        seed_number=1000,
        spoiler=True,
        patcher_configuration=PatcherConfiguration(
            menu_mod=True,
            warp_to_start=False,
        ),
        layout_configuration=LayoutConfiguration.from_params(
            trick_level_configuration=TrickLevelConfiguration(
                LayoutTrickLevel.HARD),
            elevators=LayoutElevators.RANDOMIZED,
        ),
    )

    # Uncomment this line to quickly get the new encoded permalink
    # assert expected.as_str == ""
    # print(expected.as_str)

    # Run
    link = Permalink.from_str(encoded)

    # Assert
    assert link == expected
Ejemplo n.º 2
0
def test_decode_mock_other(encoded, num_players, mocker):
    preset = MagicMock()

    def read_values(decoder: BitPackDecoder, metadata):
        decoder.decode(100, 100)
        return preset

    mock_preset_unpack: MagicMock = mocker.patch(
        "randovania.layout.preset.Preset.bit_pack_unpack",
        side_effect=read_values)

    expected = Permalink(
        seed_number=1000,
        spoiler=True,
        presets={i: preset
                 for i in range(num_players)},
    )
    preset.bit_pack_encode.return_value = [(0, 100), (5, 100)]

    # Uncomment this line to quickly get the new encoded permalink
    # assert expected.as_base64_str == ""
    # print(expected.as_base64_str)

    # Run
    round_trip = expected.as_base64_str
    link = Permalink.from_str(encoded)

    # Assert
    assert link == expected
    assert round_trip == encoded
    mock_preset_unpack.assert_called_once_with(ANY, {"manager": ANY})
Ejemplo n.º 3
0
def test_decode_mock_other(
    mock_packer_unpack: MagicMock,
    mock_layout_unpack: MagicMock,
):

    encoded = "gAAAfRggLQ=="
    patcher_configuration = mock_packer_unpack.return_value
    layout_configuration = mock_layout_unpack.return_value

    expected = Permalink(
        seed_number=1000,
        spoiler=True,
        patcher_configuration=patcher_configuration,
        layout_configuration=layout_configuration,
    )
    patcher_configuration.bit_pack_encode.return_value = []
    layout_configuration.bit_pack_encode.return_value = []
    mock_layout_unpack.return_value.game_data = {"test": True}

    # Uncomment this line to quickly get the new encoded permalink
    # assert expected.as_str == ""
    # print(expected.as_str)

    # Run
    link = Permalink.from_str(encoded)
    round_trip = expected.as_str

    # Assert
    assert link == expected
    assert encoded == round_trip
    mock_packer_unpack.assert_called_once()
    mock_layout_unpack.assert_called_once()
    patcher_configuration.bit_pack_encode.assert_called_once_with({})
    layout_configuration.bit_pack_encode.assert_called_once_with({})
Ejemplo n.º 4
0
def test_round_trip(spoiler: bool, layout: dict, default_preset, mocker):
    # Setup
    random_uuid = uuid.uuid4()
    mocker.patch("uuid.uuid4", return_value=random_uuid)

    preset = Preset(
        name="{} Custom".format(default_preset.name),
        description="A customized preset.",
        uuid=random_uuid,
        base_preset_uuid=default_preset.uuid,
        game=default_preset.game,
        configuration=dataclasses.replace(default_preset.configuration,
                                          **layout),
    )

    link = Permalink(
        seed_number=1000,
        spoiler=spoiler,
        presets={0: preset},
    )

    # Run
    after = Permalink.from_str(link.as_base64_str)

    # Assert
    assert link == after
Ejemplo n.º 5
0
def test_decode_old_version(permalink: str, version: int):
    with pytest.raises(ValueError) as exp:
        Permalink.from_str(permalink)
    assert str(
        exp.value) == ("Given permalink has version {}, but this Randovania "
                       "support only permalink of version {}.".format(
                           version, Permalink.current_version()))
Ejemplo n.º 6
0
def test_round_trip(spoiler: bool, patcher: PatcherConfiguration,
                    layout: LayoutConfiguration):
    # Setup
    link = Permalink(
        seed_number=1000,
        spoiler=spoiler,
        patcher_configuration=patcher,
        layout_configuration=layout,
    )

    # Run
    after = Permalink.from_str(link.as_str)

    # Assert
    assert link == after
Ejemplo n.º 7
0
def _async_create_description(
    permalink: Permalink,
    status_update: Callable[[str], None],
    attempts: int,
) -> LayoutDescription:
    """
    :param permalink:
    :param status_update:
    :return:
    """
    rng = Random(permalink.as_bytes)

    presets = {
        i: permalink.get_preset(i)
        for i in range(permalink.player_count)
    }

    retrying = tenacity.Retrying(
        stop=tenacity.stop_after_attempt(attempts),
        retry=tenacity.retry_if_exception_type(UnableToGenerate),
        reraise=True)

    filler_results = retrying(_create_pools_and_fill, rng, presets,
                              status_update)
    all_patches = _distribute_remaining_items(rng,
                                              filler_results.player_results)
    return LayoutDescription(
        permalink=permalink,
        version=VERSION,
        all_patches=all_patches,
        item_order=filler_results.action_log,
    )
Ejemplo n.º 8
0
def batch_distribute_command_logic(args):
    finished_count = 0

    validate: bool = args.validate

    output_dir: Path = args.output_dir
    output_dir.mkdir(parents=True, exist_ok=True)

    base_permalink = Permalink.from_str(args.permalink)

    def callback(result):
        nonlocal finished_count
        finished_count += 1
        print("Finished seed in {} seconds. At {} of {} seeds.".format(result, finished_count, args.seed_count))

    def error_callback(e):
        nonlocal finished_count
        finished_count += 1
        print("Failed to generate seed: {}".format(e))

    with multiprocessing.Pool() as pool:
        for seed_number in range(base_permalink.seed_number, base_permalink.seed_number + args.seed_count):
            pool.apply_async(
                func=batch_distribute_helper,
                args=(base_permalink, seed_number, output_dir, validate),
                callback=callback,
                error_callback=error_callback,
            )
        pool.close()
        pool.join()
Ejemplo n.º 9
0
def test_batch_distribute_helper(
    mock_perf_counter: MagicMock,
    mock_generate_description: MagicMock,
):
    # Setup
    base_permalink = MagicMock()
    seed_number = 5000
    validate = MagicMock()
    output_dir = MagicMock()
    timeout = 67

    expected_permalink = Permalink(
        seed_number=seed_number,
        spoiler=True,
        presets=base_permalink.presets,
    )

    mock_perf_counter.side_effect = [1000, 5000]

    # Run
    delta_time = batch_distribute.batch_distribute_helper(
        base_permalink, seed_number, timeout, validate, output_dir)

    # Assert
    mock_generate_description.assert_called_once_with(
        permalink=expected_permalink,
        status_update=None,
        validate_after_generation=validate,
        timeout=timeout)
    assert delta_time == 4000
    output_dir.joinpath.assert_called_once_with("{}.json".format(seed_number))
    mock_generate_description.return_value.save_to_file.assert_called_once_with(
        output_dir.joinpath.return_value)
Ejemplo n.º 10
0
def batch_distribute_helper(
    base_permalink,
    seed_number: int,
    timeout: int,
    validate: bool,
    output_dir: Path,
) -> float:
    from randovania.generator import generator
    from randovania.layout.permalink import Permalink

    permalink = Permalink(
        seed_number=seed_number,
        spoiler=True,
        presets=typing.cast(Permalink, base_permalink).presets,
    )

    start_time = time.perf_counter()
    description = asyncio.run(
        generator.generate_and_validate_description(
            permalink=permalink,
            status_update=None,
            validate_after_generation=validate,
            timeout=timeout,
            attempts=0,
        ))
    delta_time = time.perf_counter() - start_time

    description.save_to_file(
        output_dir.joinpath("{}.{}".format(seed_number,
                                           description.file_extension())))
    return delta_time
Ejemplo n.º 11
0
def _test_data(default_preset):
    data = default_data.decode_default_prime2()
    game = data_reader.decode_data(data)
    permalink = Permalink(
        seed_number=15000,
        spoiler=True,
        presets={0: default_preset},
    )
    configuration = permalink.get_preset(0).layout_configuration
    patches = game.create_game_patches()
    patches = patches.assign_gate_assignment(
        base_patches_factory.gate_assignment_for_configuration(
            configuration, game.resource_database, Random(15000)))
    game, state = logic_bootstrap(configuration, game, patches)

    return game, state, permalink
Ejemplo n.º 12
0
def test_batch_distribute_helper(
    mock_perf_counter: MagicMock,
    mock_generate_description: MagicMock,
):
    # Setup
    base_permalink = MagicMock()
    seed_number = 5000
    validate = MagicMock()
    output_dir = MagicMock()

    expected_permalink = Permalink(
        seed_number=seed_number,
        spoiler=True,
        patcher_configuration=base_permalink.patcher_configuration,
        layout_configuration=base_permalink.layout_configuration,
    )

    mock_perf_counter.side_effect = [1000, 5000]

    # Run
    delta_time = batch_distribute.batch_distribute_helper(
        base_permalink, seed_number, validate, output_dir)

    # Assert
    mock_generate_description.assert_called_once_with(expected_permalink, None,
                                                      validate)
    assert delta_time == 4000
    output_dir.joinpath.assert_called_once_with("{}.json".format(seed_number))
    mock_generate_description.return_value.save_to_file.assert_called_once_with(
        output_dir.joinpath.return_value)
Ejemplo n.º 13
0
def test_round_trip_generated_patches(echoes_game_data, preset_manager):
    # Setup
    preset = dataclasses.replace(
        preset_manager.default_preset,
        layout_configuration=dataclasses.replace(
            preset_manager.default_preset.layout_configuration,
            trick_level_configuration=TrickLevelConfiguration(
                global_level=LayoutTrickLevel.MINIMAL_RESTRICTIONS,
                specific_levels={},
            )
        )
    )

    patches = generator._create_randomized_patches(
        permalink=Permalink(
            seed_number=1000,
            spoiler=True,
            preset=preset,
        ),
        game=data_reader.decode_data(echoes_game_data),
        status_update=lambda x: None,
    )

    # Run
    encoded = game_patches_serializer.serialize(patches, echoes_game_data)
    decoded = game_patches_serializer.decode(encoded, preset.layout_configuration)

    # Assert
    assert patches == decoded
Ejemplo n.º 14
0
    async def on_request_presets(self, ctx: ComponentContext):
        try:
            title = ctx.origin_message.embeds[0].title
            # Trim leading and trailing `s
            permalink = Permalink.from_str(title[1:-1])

        except (IndexError, ValueError, UnsupportedPermalink) as e:
            logging.exception("Unable to find permalink on message that sent attach_presets_of_permalink")
            permalink = None

        files = []

        if permalink is not None:
            for player, preset in enumerate(permalink.parameters.presets):
                data = io.BytesIO()
                VersionedPreset.with_preset(preset).save_to_io(data)
                data.seek(0)
                files.append(
                    discord.File(data, filename=f"Player {player + 1}'s Preset.{VersionedPreset.file_extension()}")
                )

        await ctx.edit_origin(
            components=[],
            files=files,
        )
Ejemplo n.º 15
0
 def _generate_new_seed(self, spoiler: bool):
     self.generate_seed_from_permalink(
         Permalink(
             seed_number=random.randint(0, 2**31),
             spoiler=spoiler,
             preset=self._current_preset_data,
         ))
Ejemplo n.º 16
0
def test_generate_new_seed(tab, preset_manager, mocker):
    # Setup
    mock_randint: MagicMock = mocker.patch("random.randint", return_value=12341234)

    tab.window.create_preset_tree = MagicMock()
    tab.window.create_preset_tree.current_preset_data = preset_manager.default_preset
    tab.generate_seed_from_permalink = MagicMock()

    spoiler = MagicMock(spec=bool)
    retries = MagicMock(spec=int)

    # Run
    tab._generate_new_seed(spoiler, retries)

    # Assert
    tab.generate_seed_from_permalink.assert_called_once_with(
        Permalink.from_parameters(
            GeneratorParameters(
                seed_number=12341234,
                spoiler=spoiler,
                presets=[preset_manager.default_preset.get_preset()],
            )
        ), retries=retries
    )
    mock_randint.assert_called_once_with(0, 2 ** 31)
async def test_round_trip_generated_patches(default_preset):
    # Setup
    preset = dataclasses.replace(
        default_preset,
        uuid=uuid.UUID('b41fde84-1f57-4b79-8cd6-3e5a78077fa6'),
        base_preset_uuid=default_preset.uuid,
        configuration=dataclasses.replace(default_preset.configuration,
                                          trick_level=TrickLevelConfiguration(
                                              minimal_logic=True,
                                              specific_levels={},
                                              game=default_preset.game,
                                          )))

    description = await generator._create_description(
        permalink=Permalink(
            seed_number=1000,
            spoiler=True,
            presets={0: preset},
        ),
        status_update=lambda x: None,
        attempts=0,
    )
    all_patches = description.all_patches

    # Run
    encoded = game_patches_serializer.serialize(all_patches,
                                                {0: default_preset.game})
    decoded = game_patches_serializer.decode(encoded,
                                             {0: preset.configuration})

    # Assert
    assert all_patches == decoded
Ejemplo n.º 18
0
def _async_create_description(
    permalink: Permalink,
    status_update: Callable[[str], None],
) -> LayoutDescription:
    """
    :param permalink:
    :param status_update:
    :return:
    """
    rng = Random(permalink.as_str)

    presets = {
        i: permalink.get_preset(i)
        for i in range(permalink.player_count)
    }

    filler_results = _retryable_create_patches(rng, presets, status_update)
    all_patches = _distribute_remaining_items(rng,
                                              filler_results.player_results)
    return LayoutDescription(
        permalink=permalink,
        version=VERSION,
        all_patches=all_patches,
        item_order=filler_results.action_log,
    )
def test_round_trip_generated_patches(echoes_game_data, default_preset):
    # Setup
    preset = dataclasses.replace(
        default_preset,
        base_preset_name=default_preset.name,
        configuration=dataclasses.replace(
            default_preset.configuration,
            trick_level=TrickLevelConfiguration(
                minimal_logic=True,
                specific_levels={},
                game=RandovaniaGame.PRIME2,
            )
        )
    )

    all_patches = generator._async_create_description(
        permalink=Permalink(
            seed_number=1000,
            spoiler=True,
            presets={0: preset},
        ),
        status_update=lambda x: None,
        attempts=0,
    ).all_patches

    # Run
    encoded = game_patches_serializer.serialize(all_patches, {0: echoes_game_data})
    decoded = game_patches_serializer.decode(encoded, {0: preset.configuration})

    # Assert
    assert all_patches == decoded
Ejemplo n.º 20
0
def distribute_command_logic(args):
    def status_update(s):
        if args.status_update:
            print(s)

    if args.permalink is not None:
        permalink = Permalink.from_str(args.permalink)
    else:
        permalink = asyncio.run(_create_permalink(args))
        print(f"Permalink: {permalink.as_base64_str}")

    if permalink.spoiler:
        debug.set_level(args.debug)

    extra_args = {}
    if args.no_retry:
        extra_args["attempts"] = 0

    before = time.perf_counter()
    layout_description = generator.generate_description(
        permalink=permalink,
        status_update=status_update,
        validate_after_generation=args.validate,
        timeout=None,
        **extra_args)
    after = time.perf_counter()
    print("Took {} seconds. Hash: {}".format(
        after - before, layout_description.shareable_hash))

    layout_description.save_to_file(args.output_file)
Ejemplo n.º 21
0
def _create_test_layout_description(
        configuration: LayoutConfiguration,
        pickup_mapping: Iterable[int],
) -> LayoutDescription:
    """
    Creates a LayoutDescription for the given configuration, with the patches being for the given pickup_mapping
    :param configuration:
    :param pickup_mapping:
    :return:
    """
    game = data_reader.decode_data(configuration.game_data)
    pickup_database = game.pickup_database

    return LayoutDescription(
        version=VERSION,
        permalink=Permalink(
            seed_number=0,
            spoiler=True,
            patcher_configuration=PatcherConfiguration.default(),
            layout_configuration=configuration,
        ),
        patches=GamePatches.with_game(game).assign_new_pickups([
            (PickupIndex(i), pickup_database.original_pickup_mapping[PickupIndex(new_index)])
            for i, new_index in enumerate(pickup_mapping)
        ]),
        solver_path=())
Ejemplo n.º 22
0
def batch_distribute_helper(
    base_permalink: Permalink,
    seed_number: int,
    timeout: int,
    validate: bool,
    output_dir: Path,
) -> float:
    permalink = Permalink(
        seed_number=seed_number,
        spoiler=True,
        patcher_configuration=base_permalink.patcher_configuration,
        layout_configuration=base_permalink.layout_configuration,
    )

    start_time = time.perf_counter()
    description = generator.generate_description(
        permalink=permalink,
        status_update=None,
        validate_after_generation=validate,
        timeout=timeout)
    delta_time = time.perf_counter() - start_time

    description.save_to_file(output_dir.joinpath(
        "{}.json".format(seed_number)))
    return delta_time
Ejemplo n.º 23
0
def test_distribute_command_logic(mock_generate_list: MagicMock, ):
    # Setup
    args = MagicMock()
    args.trick_level = LayoutTrickLevel.HARD.value
    args.major_items_mode = False
    args.sky_temple_keys = LayoutSkyTempleKeyMode.ALL_BOSSES.value
    args.skip_item_loss = True
    args.seed = 15000
    args.output_file = "asdfasdf/qwerqwerqwer/zxcvzxcv.json"

    # Run
    echoes.distribute_command_logic(args)

    # Assert
    mock_generate_list.assert_called_once_with(permalink=Permalink(
        seed_number=args.seed,
        spoiler=True,
        patcher_configuration=PatcherConfiguration.default(),
        layout_configuration=LayoutConfiguration.from_params(
            trick_level=LayoutTrickLevel.HARD,
            sky_temple_keys=LayoutSkyTempleKeyMode.ALL_BOSSES,
            elevators=LayoutRandomizedFlag.VANILLA,
            pickup_quantities={},
            starting_location=StartingLocation.default(),
            starting_resources=StartingResources.from_non_custom_configuration(
                StartingResourcesConfiguration.VANILLA_ITEM_LOSS_DISABLED),
        ),
    ),
                                               status_update=ANY)

    save_file_mock: MagicMock = mock_generate_list.return_value.save_to_file
    save_file_mock.assert_called_once_with(Path(args.output_file))
def test_round_trip_generated_patches(echoes_game_data):
    # Setup
    configuration = LayoutConfiguration.from_params(
        trick_level_configuration=TrickLevelConfiguration(
            global_level=LayoutTrickLevel.MINIMAL_RESTRICTIONS,
            specific_levels={},
        ))

    patches = generator._create_randomized_patches(
        permalink=Permalink(
            seed_number=1000,
            spoiler=True,
            patcher_configuration=PatcherConfiguration.default(),
            layout_configuration=configuration,
        ),
        game=data_reader.decode_data(echoes_game_data),
        status_update=lambda x: None,
    )

    # Run
    encoded = game_patches_serializer.serialize(patches, echoes_game_data)
    decoded = game_patches_serializer.decode(encoded, configuration)

    # Assert
    assert patches == decoded
Ejemplo n.º 25
0
async def test_generate_game(window, mocker, preset_manager):
    mock_generate_layout: MagicMock = mocker.patch("randovania.interface_common.simplified_patcher.generate_layout")
    mock_randint: MagicMock = mocker.patch("random.randint", return_value=5000)

    spoiler = True
    game_session = MagicMock()
    game_session.presets = [preset_manager.default_preset, preset_manager.default_preset]

    window._game_session = game_session
    window._upload_layout_description = AsyncMock()
    window._admin_global_action = AsyncMock()

    # Run
    await window.generate_game(spoiler)

    # Assert
    mock_randint.assert_called_once_with(0, 2 ** 31)
    mock_generate_layout.assert_called_once_with(
        progress_update=ANY,
        permalink=Permalink(
            seed_number=mock_randint.return_value,
            spoiler=spoiler,
            presets={
                0: preset_manager.default_preset.get_preset(),
                1: preset_manager.default_preset.get_preset(),
            },
        ),
        options=window._options
    )
    window._upload_layout_description.assert_awaited_once_with(mock_generate_layout.return_value)
Ejemplo n.º 26
0
    def from_json_dict(cls, json_dict: dict) -> "LayoutDescription":
        json_dict = migrate_description(json_dict)

        has_spoiler = "game_modifications" in json_dict
        if not has_spoiler:
            raise ValueError(
                "Unable to read details of seed log with spoiler disabled")

        permalink = Permalink(
            seed_number=json_dict["info"]["seed"],
            spoiler=has_spoiler,
            presets={
                index: VersionedPreset(preset).get_preset()
                for index, preset in enumerate(json_dict["info"]["presets"])
            },
        )

        return LayoutDescription(
            version=json_dict["info"]["version"],
            permalink=permalink,
            all_patches=game_patches_serializer.decode(
                json_dict["game_modifications"], {
                    index: preset.configuration
                    for index, preset in permalink.presets.items()
                }),
            item_order=json_dict["item_order"],
        )
def test_round_trip_generated_patches(echoes_game_data, preset_manager):
    # Setup
    preset = dataclasses.replace(
        preset_manager.default_preset,
        layout_configuration=dataclasses.replace(
            preset_manager.default_preset.layout_configuration,
            trick_level_configuration=TrickLevelConfiguration(
                global_level=LayoutTrickLevel.MINIMAL_RESTRICTIONS,
                specific_levels={},
            )))

    all_patches = generator._async_create_description(
        permalink=Permalink(
            seed_number=1000,
            spoiler=True,
            presets={0: preset},
        ),
        status_update=lambda x: None,
    ).all_patches

    # Run
    encoded = game_patches_serializer.serialize(all_patches,
                                                {0: echoes_game_data})
    decoded = game_patches_serializer.decode(encoded,
                                             {0: preset.layout_configuration})

    # Assert
    assert all_patches == decoded
Ejemplo n.º 28
0
def create_permalink(args):
    from randovania.layout.permalink import Permalink
    from randovania.layout.generator_parameters import GeneratorParameters
    from randovania.interface_common.preset_manager import PresetManager

    game: RandovaniaGame = RandovaniaGame(args.game)
    preset_manager = PresetManager(None)
    presets = []
    for preset_name in args.preset_name:
        versioned = preset_manager.included_preset_with(game, preset_name)
        if versioned is None:
            raise ValueError(
                "Unknown included preset '{}' for game {}. Valid options are: {}"
                .format(preset_name, game.long_name, [
                    preset.name
                    for preset in preset_manager.included_presets.values()
                    if preset.game == game
                ]))
        presets.append(versioned.get_preset())

    seed = args.seed_number
    if seed is None:
        seed = random.randint(0, 2**31)

    return Permalink.from_parameters(
        GeneratorParameters(
            seed,
            spoiler=not args.race,
            presets=presets,
        ), )
def test_create_permalink_logic(mock_print: MagicMock, ):
    # Setup
    args = MagicMock()
    args.trick_level = LayoutTrickLevel.HARD.value
    args.major_items_mode = False
    args.sky_temple_keys = LayoutSkyTempleKeyMode.ALL_BOSSES.value
    args.skip_item_loss = True
    args.seed = 15000
    args.menu_mod = False
    args.warp_to_start = False

    # Run
    randovania.cli.commands.create_permalink.create_permalink_logic(args)

    # Assert
    permalink = Permalink(
        seed_number=args.seed,
        spoiler=True,
        patcher_configuration=PatcherConfiguration(
            menu_mod=args.menu_mod,
            warp_to_start=args.warp_to_start,
        ),
        layout_configuration=LayoutConfiguration.from_params(
            trick_level_configuration=TrickLevelConfiguration(
                LayoutTrickLevel.HARD),
            sky_temple_keys=LayoutSkyTempleKeyMode.ALL_BOSSES,
            elevators=LayoutElevators.VANILLA,
            starting_location=StartingLocation.default(),
        ),
    )

    # Assert
    mock_print.assert_called_once_with(permalink)
Ejemplo n.º 30
0
def test_round_trip(seed_hash, fake_generator_parameters, mocker):
    mock_from_bytes: MagicMock = mocker.patch(
        "randovania.layout.generator_parameters.GeneratorParameters.from_bytes",
        autospec=True,
        return_value=fake_generator_parameters)
    # Setup
    link = Permalink(parameters=fake_generator_parameters,
                     seed_hash=seed_hash,
                     randovania_version=b"0123")

    # Run
    after = Permalink.from_str(link.as_base64_str)

    # Assert
    assert link == after
    mock_from_bytes.assert_called_once_with(b"\xA0\xB0\xC0")