diff --git a/tests/e2e_tests/__init__.py b/tests/e2e_tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/e2e_tests/conftest.py b/tests/e2e_tests/conftest.py new file mode 100644 index 0000000000..9fc9faec68 --- /dev/null +++ b/tests/e2e_tests/conftest.py @@ -0,0 +1,84 @@ +import os +import re +import shlex +import signal +import subprocess +import time + +import pytest +from substrateinterface import SubstrateInterface + +from bittensor import logging +from tests.e2e_tests.utils.test_utils import ( + clone_or_update_templates, + install_templates, + template_path, + uninstall_templates, +) + + +# Fixture for setting up and tearing down a localnet.sh chain between tests +@pytest.fixture(scope="function") +def local_chain(request): + param = request.param if hasattr(request, "param") else None + # Get the environment variable for the script path + script_path = os.getenv("LOCALNET_SH_PATH") + + if not script_path: + # Skip the test if the localhost.sh path is not set + logging.warning("LOCALNET_SH_PATH env variable is not set, e2e test skipped.") + pytest.skip("LOCALNET_SH_PATH environment variable is not set.") + + # Check if param is None, and handle it accordingly + args = "" if param is None else f"{param}" + + # Compile commands to send to process + cmds = shlex.split(f"{script_path} {args}") + + # Start new node process + process = subprocess.Popen( + cmds, stdout=subprocess.PIPE, text=True, preexec_fn=os.setsid + ) + + # Pattern match indicates node is compiled and ready + pattern = re.compile(r"Imported #1") + + # install neuron templates + logging.info("downloading and installing neuron templates from github") + templates_dir = clone_or_update_templates() + install_templates(templates_dir) + + timestamp = int(time.time()) + + def wait_for_node_start(process, pattern): + for line in process.stdout: + print(line.strip()) + # 10 min as timeout + if int(time.time()) - timestamp > 10 * 60: + print("Subtensor not started in time") + break + if pattern.search(line): + print("Node started!") + break + + wait_for_node_start(process, pattern) + + # Run the test, passing in substrate interface + yield SubstrateInterface(url="ws://127.0.0.1:9945") + + # Terminate the process group (includes all child processes) + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + + # Give some time for the process to terminate + time.sleep(1) + + # If the process is not terminated, send SIGKILL + if process.poll() is None: + os.killpg(os.getpgid(process.pid), signal.SIGKILL) + + # Ensure the process has terminated + process.wait() + + # uninstall templates + logging.info("uninstalling neuron templates") + uninstall_templates(template_path) diff --git a/tests/e2e_tests/test_axon.py b/tests/e2e_tests/test_axon.py new file mode 100644 index 0000000000..ed6be97847 --- /dev/null +++ b/tests/e2e_tests/test_axon.py @@ -0,0 +1,128 @@ +import asyncio +import sys + +import pytest + +import bittensor +from bittensor import logging +from bittensor.utils import networking +from tests.e2e_tests.utils.chain_interactions import register_neuron, register_subnet +from tests.e2e_tests.utils.test_utils import ( + setup_wallet, + template_path, + templates_repo, +) + + +@pytest.mark.asyncio +async def test_axon(local_chain): + """ + Test the Axon mechanism and successful registration on the network. + + Steps: + 1. Register a subnet and register Alice + 2. Check if metagraph.axon is updated and check axon attributes + 3. Run Alice as a miner on the subnet + 4. Check the metagraph again after running the miner and verify all attributes + Raises: + AssertionError: If any of the checks or verifications fail + """ + + logging.info("Testing test_axon") + + netuid = 1 + # Register root as Alice - the subnet owner + alice_keypair, wallet = setup_wallet("//Alice") + + # Register a subnet, netuid 1 + assert register_subnet(local_chain, wallet), "Subnet wasn't created" + + # Verify subnet created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [netuid] + ).serialize(), "Subnet wasn't created successfully" + + # Register Alice to the network + assert register_neuron( + local_chain, wallet, netuid + ), f"Neuron wasn't registered to subnet {netuid}" + + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + + # Validate current metagraph stats + old_axon = metagraph.axons[0] + assert len(metagraph.axons) == 1, f"Expected 1 axon, but got {len(metagraph.axons)}" + assert old_axon.hotkey == alice_keypair.ss58_address, "Hotkey mismatch for the axon" + assert ( + old_axon.coldkey == alice_keypair.ss58_address + ), "Coldkey mismatch for the axon" + assert old_axon.ip == "0.0.0.0", f"Expected IP 0.0.0.0, but got {old_axon.ip}" + assert old_axon.port == 0, f"Expected port 0, but got {old_axon.port}" + assert old_axon.ip_type == 0, f"Expected IP type 0, but got {old_axon.ip_type}" + + # Prepare to run the miner + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/miner.py"', + "--no_prompt", + "--netuid", + str(netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + wallet.path, + "--wallet.name", + wallet.name, + "--wallet.hotkey", + "default", + ] + ) + + # Run the miner in the background + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + logging.info("Neuron Alice is now mining") + + # Waiting for 5 seconds for metagraph to be updated + await asyncio.sleep(5) + + # Refresh the metagraph + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + updated_axon = metagraph.axons[0] + external_ip = networking.get_external_ip() + + # Assert updated attributes + assert ( + len(metagraph.axons) == 1 + ), f"Expected 1 axon, but got {len(metagraph.axons)} after mining" + + assert ( + len(metagraph.neurons) == 1 + ), f"Expected 1 neuron, but got {len(metagraph.neurons)}" + + assert ( + updated_axon.ip == external_ip + ), f"Expected IP {external_ip}, but got {updated_axon.ip}" + + assert ( + updated_axon.ip_type == networking.ip_version(external_ip) + ), f"Expected IP type {networking.ip_version(external_ip)}, but got {updated_axon.ip_type}" + + assert updated_axon.port == 8091, f"Expected port 8091, but got {updated_axon.port}" + + assert ( + updated_axon.hotkey == alice_keypair.ss58_address + ), "Hotkey mismatch after mining" + + assert ( + updated_axon.coldkey == alice_keypair.ss58_address + ), "Coldkey mismatch after mining" + + logging.info("✅ Passed test_axon") diff --git a/tests/e2e_tests/test_dendrite.py b/tests/e2e_tests/test_dendrite.py new file mode 100644 index 0000000000..d7e3e6ff61 --- /dev/null +++ b/tests/e2e_tests/test_dendrite.py @@ -0,0 +1,136 @@ +import asyncio +import sys + +import pytest + +import bittensor +from bittensor import logging, Subtensor + +from tests.e2e_tests.utils.test_utils import ( + setup_wallet, + template_path, + templates_repo, +) +from tests.e2e_tests.utils.chain_interactions import ( + register_neuron, + register_subnet, + add_stake, + wait_epoch, +) + + +@pytest.mark.asyncio +async def test_dendrite(local_chain): + """ + Test the Dendrite mechanism + + Steps: + 1. Register a subnet through Alice + 2. Register Bob as a validator + 3. Add stake to Bob and ensure neuron is not a validator yet + 4. Run Bob as a validator and wait epoch + 5. Ensure Bob's neuron has all correct attributes of a validator + Raises: + AssertionError: If any of the checks or verifications fail + """ + + logging.info("Testing test_dendrite") + netuid = 1 + + # Register root as Alice - the subnet owner + alice_keypair, alice_wallet = setup_wallet("//Alice") + + # Register a subnet, netuid 1 + assert register_subnet(local_chain, alice_wallet), "Subnet wasn't created" + + # Verify subnet created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [netuid] + ).serialize(), "Subnet wasn't created successfully" + + # Register Bob + bob_keypair, bob_wallet = setup_wallet("//Bob") + + # Register Bob to the network + assert register_neuron( + local_chain, bob_wallet, netuid + ), f"Neuron wasn't registered to subnet {netuid}" + + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + subtensor = Subtensor(network="ws://localhost:9945") + + # Assert one neuron is Bob + assert len(subtensor.neurons(netuid=netuid)) == 1 + neuron = metagraph.neurons[0] + assert neuron.hotkey == bob_keypair.ss58_address + assert neuron.coldkey == bob_keypair.ss58_address + + # Assert stake is 0 + assert neuron.stake.tao == 0 + + # Stake to become to top neuron after the first epoch + assert add_stake(local_chain, bob_wallet, bittensor.Balance.from_tao(10_000)) + + # Refresh metagraph + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + old_neuron = metagraph.neurons[0] + + # Assert stake is 10000 + assert ( + old_neuron.stake.tao == 10_000.0 + ), f"Expected 10_000.0 staked TAO, but got {neuron.stake.tao}" + + # Assert neuron is not a validator yet + assert old_neuron.active is True + assert old_neuron.validator_permit is False + assert old_neuron.validator_trust == 0.0 + assert old_neuron.pruning_score == 0 + + # Prepare to run the validator + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/validator.py"', + "--no_prompt", + "--netuid", + str(netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + bob_wallet.path, + "--wallet.name", + bob_wallet.name, + "--wallet.hotkey", + "default", + ] + ) + + # Run the validator in the background + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logging.info("Neuron Alice is now validating") + await asyncio.sleep( + 5 + ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data + + await wait_epoch(subtensor, netuid=netuid) + + # Refresh metagraph + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + + # Refresh validator neuron + updated_neuron = metagraph.neurons[0] + + assert len(metagraph.neurons) == 1 + assert updated_neuron.active is True + assert updated_neuron.validator_permit is True + assert updated_neuron.hotkey == bob_keypair.ss58_address + assert updated_neuron.coldkey == bob_keypair.ss58_address + assert updated_neuron.pruning_score != 0 + + logging.info("✅ Passed test_dendrite") diff --git a/tests/e2e_tests/test_incentive.py b/tests/e2e_tests/test_incentive.py new file mode 100644 index 0000000000..2c39461843 --- /dev/null +++ b/tests/e2e_tests/test_incentive.py @@ -0,0 +1,181 @@ +import asyncio +import sys + +import pytest + +import bittensor +from bittensor import Subtensor, logging +from tests.e2e_tests.utils.chain_interactions import ( + add_stake, + register_neuron, + register_subnet, + wait_epoch, +) +from tests.e2e_tests.utils.test_utils import ( + setup_wallet, + template_path, + templates_repo, +) + + +@pytest.mark.asyncio +async def test_incentive(local_chain): + """ + Test the incentive mechanism and interaction of miners/validators + + Steps: + 1. Register a subnet and register Alice & Bob + 2. Add Stake by Alice + 3. Run Alice as validator & Bob as miner. Wait Epoch + 4. Verify miner has correct: trust, rank, consensus, incentive + 5. Verify validator has correct: validator_permit, validator_trust, dividends, stake + Raises: + AssertionError: If any of the checks or verifications fail + """ + + logging.info("Testing test_incentive") + netuid = 1 + + # Register root as Alice - the subnet owner and validator + alice_keypair, alice_wallet = setup_wallet("//Alice") + register_subnet(local_chain, alice_wallet) + + # Verify subnet created successfully + assert local_chain.query( + "SubtensorModule", "NetworksAdded", [netuid] + ).serialize(), "Subnet wasn't created successfully" + + # Register Bob as miner + bob_keypair, bob_wallet = setup_wallet("//Bob") + + # Register Alice as a neuron on the subnet + register_neuron(local_chain, alice_wallet, netuid) + + # Register Bob as a neuron on the subnet + register_neuron(local_chain, bob_wallet, netuid) + + subtensor = Subtensor(network="ws://localhost:9945") + # Assert two neurons are in network + assert ( + len(subtensor.neurons(netuid=netuid)) == 2 + ), "Alice & Bob not registered in the subnet" + + # Alice to stake to become to top neuron after the first epoch + add_stake(local_chain, alice_wallet, bittensor.Balance.from_tao(10_000)) + + # Prepare to run Bob as miner + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/miner.py"', + "--no_prompt", + "--netuid", + str(netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + bob_wallet.path, + "--wallet.name", + bob_wallet.name, + "--wallet.hotkey", + "default", + "--logging.trace", + ] + ) + + # Run Bob as miner in the background + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logging.info("Neuron Bob is now mining") + await asyncio.sleep( + 5 + ) # wait for 5 seconds for the metagraph to refresh with latest data + + # Prepare to run Alice as validator + cmd = " ".join( + [ + f"{sys.executable}", + f'"{template_path}{templates_repo}/neurons/validator.py"', + "--no_prompt", + "--netuid", + str(netuid), + "--subtensor.network", + "local", + "--subtensor.chain_endpoint", + "ws://localhost:9945", + "--wallet.path", + alice_wallet.path, + "--wallet.name", + alice_wallet.name, + "--wallet.hotkey", + "default", + "--logging.trace", + ] + ) + + # Run Alice as validator in the background + await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + logging.info("Neuron Alice is now validating") + await asyncio.sleep( + 5 + ) # wait for 5 seconds for the metagraph and subtensor to refresh with latest data + + # Get latest metagraph + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + + # Get current miner/validator stats + bob_neuron = metagraph.neurons[1] + assert bob_neuron.incentive == 0 + assert bob_neuron.consensus == 0 + assert bob_neuron.rank == 0 + assert bob_neuron.trust == 0 + + alice_neuron = metagraph.neurons[0] + assert alice_neuron.validator_permit is False + assert alice_neuron.dividends == 0 + assert alice_neuron.stake.tao == 10_000.0 + assert alice_neuron.validator_trust == 0 + + # Wait until next epoch + await wait_epoch(subtensor) + + # Set weights by Alice on the subnet + subtensor._do_set_weights( + wallet=alice_wallet, + uids=[1], + vals=[65535], + netuid=netuid, + version_key=0, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + logging.info("Alice neuron set weights successfully") + + await wait_epoch(subtensor) + + # Refresh metagraph + metagraph = bittensor.metagraph(netuid=netuid, network="ws://localhost:9945") + + # Get current emissions and validate that Alice has gotten tao + bob_neuron = metagraph.neurons[1] + assert bob_neuron.incentive == 1 + assert bob_neuron.consensus == 1 + assert bob_neuron.rank == 1 + assert bob_neuron.trust == 1 + + alice_neuron = metagraph.neurons[0] + assert alice_neuron.validator_permit is True + assert alice_neuron.dividends == 1 + assert alice_neuron.stake.tao == 10_000.0 + assert alice_neuron.validator_trust == 1 + + logging.info("✅ Passed test_incentive") diff --git a/tests/e2e_tests/test_transfer.py b/tests/e2e_tests/test_transfer.py new file mode 100644 index 0000000000..9ec501d5bd --- /dev/null +++ b/tests/e2e_tests/test_transfer.py @@ -0,0 +1,52 @@ +from bittensor import Subtensor, logging +from bittensor.core.subtensor import transfer_extrinsic +from tests.e2e_tests.utils.test_utils import setup_wallet + + +def test_transfer(local_chain): + """ + Test the transfer mechanism on the chain + + Steps: + 1. Create a wallet for Alice + 2. Calculate existing balance and transfer 2 Tao + 3. Calculate balance after extrinsic call and verify calculations + Raises: + AssertionError: If any of the checks or verifications fail + """ + + logging.info("Testing test_transfer") + + # Set up Alice wallet + keypair, wallet = setup_wallet("//Alice") + + # Account details before transfer + acc_before = local_chain.query("System", "Account", [keypair.ss58_address]) + + # Transfer Tao using extrinsic + subtensor = Subtensor(network="ws://localhost:9945") + transfer_extrinsic( + subtensor=subtensor, + wallet=wallet, + dest="5GpzQgpiAKHMWNSH3RN4GLf96GVTDct9QxYEFAY7LWcVzTbx", + amount=2, + wait_for_finalization=True, + wait_for_inclusion=True, + prompt=False, + ) + + # Account details after transfer + acc_after = local_chain.query("System", "Account", [keypair.ss58_address]) + + # Transfer calculation assertions + expected_transfer = 2_000_000_000 + tolerance = 200_000 # Tx fee tolerance + + actual_difference = ( + acc_before.value["data"]["free"] - acc_after.value["data"]["free"] + ) + assert ( + expected_transfer <= actual_difference <= expected_transfer + tolerance + ), f"Expected transfer with tolerance: {expected_transfer} <= {actual_difference} <= {expected_transfer + tolerance}" + + logging.info("✅ Passed test_transfer") diff --git a/tests/e2e_tests/utils/chain_interactions.py b/tests/e2e_tests/utils/chain_interactions.py new file mode 100644 index 0000000000..f0797770dc --- /dev/null +++ b/tests/e2e_tests/utils/chain_interactions.py @@ -0,0 +1,126 @@ +""" +This module provides functions interacting with the chain for end to end testing; +these are not present in btsdk but are required for e2e tests +""" + +import asyncio + +from substrateinterface import SubstrateInterface + +import bittensor +from bittensor import logging + + +def add_stake( + substrate: SubstrateInterface, wallet: bittensor.wallet, amount: bittensor.Balance +) -> bool: + """ + Adds stake to a hotkey using SubtensorModule. Mimics command of adding stake + """ + stake_call = substrate.compose_call( + call_module="SubtensorModule", + call_function="add_stake", + call_params={"hotkey": wallet.hotkey.ss58_address, "amount_staked": amount.rao}, + ) + extrinsic = substrate.create_signed_extrinsic( + call=stake_call, keypair=wallet.coldkey + ) + response = substrate.submit_extrinsic( + extrinsic, wait_for_finalization=True, wait_for_inclusion=True + ) + response.process_events() + return response.is_success + + +def register_subnet(substrate: SubstrateInterface, wallet: bittensor.wallet) -> bool: + """ + Registers a subnet on the chain using wallet. Mimics register subnet command. + """ + register_call = substrate.compose_call( + call_module="SubtensorModule", + call_function="register_network", + call_params={"immunity_period": 0, "reg_allowed": True}, + ) + extrinsic = substrate.create_signed_extrinsic( + call=register_call, keypair=wallet.coldkey + ) + response = substrate.submit_extrinsic( + extrinsic, wait_for_finalization=True, wait_for_inclusion=True + ) + response.process_events() + return response.is_success + + +def register_neuron( + substrate: SubstrateInterface, wallet: bittensor.wallet, netuid: int +) -> bool: + """ + Registers a neuron on a subnet. Mimics subnet register command. + """ + neuron_register_call = substrate.compose_call( + call_module="SubtensorModule", + call_function="burned_register", + call_params={ + "netuid": netuid, + "hotkey": wallet.hotkey.ss58_address, + }, + ) + extrinsic = substrate.create_signed_extrinsic( + call=neuron_register_call, keypair=wallet.coldkey + ) + response = substrate.submit_extrinsic( + extrinsic, wait_for_finalization=True, wait_for_inclusion=True + ) + response.process_events() + return response.is_success + + +async def wait_epoch(subtensor, netuid=1): + """ + Waits for the next epoch to start on a specific subnet. + + Queries the tempo value from the Subtensor module and calculates the + interval based on the tempo. Then waits for the next epoch to start + by monitoring the current block number. + + Raises: + Exception: If the tempo cannot be determined from the chain. + """ + q_tempo = [ + v.value + for [k, v] in subtensor.query_map_subtensor("Tempo") + if k.value == netuid + ] + if len(q_tempo) == 0: + raise Exception("could not determine tempo") + tempo = q_tempo[0] + logging.info(f"tempo = {tempo}") + await wait_interval(tempo, subtensor, netuid) + + +async def wait_interval(tempo, subtensor, netuid=1): + """ + Waits until the next tempo interval starts for a specific subnet. + + Calculates the next tempo block start based on the current block number + and the provided tempo, then enters a loop where it periodically checks + the current block number until the next tempo interval starts. + """ + interval = tempo + 1 + current_block = subtensor.get_current_block() + last_epoch = current_block - 1 - (current_block + netuid + 1) % interval + next_tempo_block_start = last_epoch + interval + last_reported = None + while current_block < next_tempo_block_start: + await asyncio.sleep( + 1 + ) # Wait for 1 second before checking the block number again + current_block = subtensor.get_current_block() + if last_reported is None or current_block - last_reported >= 10: + last_reported = current_block + print( + f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" + ) + logging.info( + f"Current Block: {current_block} Next tempo for netuid {netuid} at: {next_tempo_block_start}" + ) diff --git a/tests/e2e_tests/utils/test_utils.py b/tests/e2e_tests/utils/test_utils.py new file mode 100644 index 0000000000..9061df791f --- /dev/null +++ b/tests/e2e_tests/utils/test_utils.py @@ -0,0 +1,84 @@ +import os +import shutil +import subprocess +import sys +from typing import Tuple + +from substrateinterface import Keypair + +import bittensor + +template_path = os.getcwd() + "/neurons/" +templates_repo = "templates repository" + + +def setup_wallet(uri: str) -> Tuple[Keypair, bittensor.wallet]: + """ + Sets up a wallet using the provided URI. + + This function creates a keypair from the given URI and initializes a wallet + at a temporary path. It sets the coldkey, coldkeypub, and hotkey for the wallet + using the generated keypair. + + Side Effects: + - Creates a wallet in a temporary directory. + - Sets keys in the wallet without encryption and with overwriting enabled. + """ + keypair = Keypair.create_from_uri(uri) + wallet_path = "/tmp/btcli-e2e-wallet-{}".format(uri.strip("/")) + wallet = bittensor.wallet(path=wallet_path) + wallet.set_coldkey(keypair=keypair, encrypt=False, overwrite=True) + wallet.set_coldkeypub(keypair=keypair, encrypt=False, overwrite=True) + wallet.set_hotkey(keypair=keypair, encrypt=False, overwrite=True) + return keypair, wallet + + +def clone_or_update_templates(specific_commit=None): + """ + Clones or updates the Bittensor subnet template repository. + + This function clones the Bittensor subnet template repository if it does not + already exist in the specified installation directory. If the repository already + exists, it updates it by pulling the latest changes. Optionally, it can check out + a specific commit if the `specific_commit` variable is set. + """ + install_dir = template_path + repo_mapping = { + templates_repo: "https://github.com/opentensor/bittensor-subnet-template.git", + } + + os.makedirs(install_dir, exist_ok=True) + os.chdir(install_dir) + + for repo, git_link in repo_mapping.items(): + if not os.path.exists(repo): + print(f"\033[94mCloning {repo}...\033[0m") + subprocess.run(["git", "clone", git_link, repo], check=True) + else: + print(f"\033[94mUpdating {repo}...\033[0m") + os.chdir(repo) + subprocess.run(["git", "pull"], check=True) + os.chdir("..") + + # For pulling specific commit versions of repo + if specific_commit: + os.chdir(templates_repo) + print( + f"\033[94mChecking out commit {specific_commit} in {templates_repo}...\033[0m" + ) + subprocess.run(["git", "checkout", specific_commit], check=True) + os.chdir("..") + + return install_dir + templates_repo + "/" + + +def install_templates(install_dir): + subprocess.check_call([sys.executable, "-m", "pip", "install", install_dir]) + + +def uninstall_templates(install_dir): + subprocess.check_call( + [sys.executable, "-m", "pip", "uninstall", "bittensor_subnet_template", "-y"] + ) + # Delete everything in directory + shutil.rmtree(install_dir)