From ec2e3192191934140043e1646b5f52a36c27940a Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Thu, 5 Oct 2023 21:13:25 -0700 Subject: [PATCH] Nuke code (#45) --- .github/workflows/ci.yaml | 67 ------- .gitignore | 2 - Cargo.toml | 25 --- README.md | 2 +- bin/forbid | 13 -- justfile | 15 -- rustfmt.toml | 6 - src/directive.rs | 8 - src/error.rs | 26 --- src/lib.rs | 20 -- src/main.rs | 73 ------- src/runestone.rs | 398 -------------------------------------- src/varint.rs | 110 ----------- tests/decipher.rs | 79 -------- tests/lib.rs | 15 -- tests/server.rs | 45 ----- 16 files changed, 1 insertion(+), 903 deletions(-) delete mode 100644 .github/workflows/ci.yaml delete mode 100644 .gitignore delete mode 100644 Cargo.toml delete mode 100755 bin/forbid delete mode 100644 justfile delete mode 100644 rustfmt.toml delete mode 100644 src/directive.rs delete mode 100644 src/error.rs delete mode 100644 src/lib.rs delete mode 100644 src/main.rs delete mode 100644 src/runestone.rs delete mode 100644 src/varint.rs delete mode 100644 tests/decipher.rs delete mode 100644 tests/lib.rs delete mode 100644 tests/server.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 4571166..0000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,67 +0,0 @@ -name: CI - -on: - push: - branches: - - master - pull_request: - branches: - - master - -defaults: - run: - shell: bash - -env: - RUSTFLAGS: --deny warnings - -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Install Rust Toolchain Components - uses: actions-rs/toolchain@v1 - with: - components: clippy, rustfmt - override: true - toolchain: stable - - - uses: Swatinem/rust-cache@v2 - - - name: Clippy - run: cargo clippy --all --all-targets - - - name: Format - run: cargo fmt --all -- --check - - - name: Check for Forbidden Words - run: | - sudo apt-get install ripgrep - ./bin/forbid - - test: - strategy: - matrix: - os: - - macos-latest - - ubuntu-latest - - windows-latest - - runs-on: ${{matrix.os}} - - steps: - - uses: actions/checkout@v2 - - - name: Install Rust Toolchain Components - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - - - uses: Swatinem/rust-cache@v2 - - - name: Test - run: cargo test --all diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 4fffb2f..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index 7a77d7b..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "runestone" -version = "0.0.0" -edition = "2021" -description = "Runes" -license = "CC0-1.0" -autotests = false - -[dependencies] -anyhow = "1.0.75" -axum = "0.6.1" -axum-server = "0.5.0" -bitcoin = "0.30.1" -clap = { version = "4.4.5", features = ["derive"] } -serde = { version = "1.0.188", features = ["derive"] } -serde_json = "1.0.107" -tokio = { version = "1.17.0", features = ["rt-multi-thread"] } - -[dev-dependencies] -executable-path = "1.0.0" -reqwest = { version = "0.11.20", features = ["blocking"] } - -[[test]] -name = "integration" -path = "tests/lib.rs" diff --git a/README.md b/README.md index 74feebe..0fa53fe 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ runestone Warning ------- -`runestone` implements runes, a fungible token protocol for Bitcoin. +This repo is for discussion of runes, a fungible token protocol for Bitcoin. Fungible tokens are, without exaggeration and nearly without exception, a vile abyss of hopium, scams, and incompetence. diff --git a/bin/forbid b/bin/forbid deleted file mode 100755 index a5a82f1..0000000 --- a/bin/forbid +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -which rg > /dev/null - -! rg \ - --glob '!bin/forbid' \ - --glob '!docs/src/bounty/frequency.tsv' \ - --glob '!docs/po/*' \ - --ignore-case \ - 'dbg!|fixme|todo|xxx' \ - . diff --git a/justfile b/justfile deleted file mode 100644 index 1a80249..0000000 --- a/justfile +++ /dev/null @@ -1,15 +0,0 @@ -set positional-arguments - -watch +args='test': - cargo watch --clear --exec '{{args}}' - -ci: clippy forbid - cargo fmt -- --check - cargo test --all - cargo test --all -- --ignored - -forbid: - ./bin/forbid - -clippy: - cargo clippy --all --all-targets -- -D warnings diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index 84ec2a7..0000000 --- a/rustfmt.toml +++ /dev/null @@ -1,6 +0,0 @@ -edition = "2018" -max_width = 100 -newline_style = "Unix" -tab_spaces = 2 -use_field_init_shorthand = true -use_try_shorthand = true diff --git a/src/directive.rs b/src/directive.rs deleted file mode 100644 index 0097d44..0000000 --- a/src/directive.rs +++ /dev/null @@ -1,8 +0,0 @@ -use super::*; - -#[derive(Default, Serialize, Debug, PartialEq)] -pub struct Directive { - pub id: u128, - pub amount: u128, - pub output: u128, -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 5fb4d0f..0000000 --- a/src/error.rs +++ /dev/null @@ -1,26 +0,0 @@ -use super::*; - -#[derive(Debug, PartialEq)] -pub enum Error { - Script(script::Error), - Opcode(opcodes::All), - Varint, -} - -impl From for Error { - fn from(error: script::Error) -> Self { - Self::Script(error) - } -} - -impl Display for Error { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - Self::Script(err) => write!(f, "failed to parse script: {err}"), - Self::Opcode(op) => write!(f, "non-push opcode {op} in payload"), - Self::Varint => write!(f, "varint over maximum value"), - } - } -} - -impl std::error::Error for Error {} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 3de60ca..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -use { - self::directive::Directive, - bitcoin::{ - opcodes, - script::{self, Instruction}, - Transaction, - }, - error::Error, - serde::Serialize, - std::fmt::{self, Display, Formatter}, -}; - -pub use runestone::Runestone; - -mod directive; -mod error; -mod runestone; -mod varint; - -type Result = std::result::Result; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 2d12df0..0000000 --- a/src/main.rs +++ /dev/null @@ -1,73 +0,0 @@ -use { - axum::routing::get, - axum::Router, - bitcoin::{consensus::Decodable, Transaction}, - clap::{ - builder::{ - styling::{AnsiColor, Effects}, - Styles, - }, - Parser, - }, - runestone::Runestone, - std::{error::Error, io, net::ToSocketAddrs}, - tokio::runtime::Runtime, -}; - -#[derive(Parser)] -#[command( - version, - styles = Styles::styled() - .header(AnsiColor::Green.on_default() | Effects::BOLD) - .usage(AnsiColor::Green.on_default() | Effects::BOLD) - .literal(AnsiColor::Blue.on_default() | Effects::BOLD) - .placeholder(AnsiColor::Cyan.on_default())) -] -enum Subcommand { - #[command( - about = "Read a bitcoin transaction from standard input and print a JSON representation of its runestone." - )] - Decipher, - #[command(about = "Start the explorer.")] - Server(Server), -} - -#[derive(Parser)] -struct Server { - #[arg( - long, - help = "Listen on for incoming HTTP requests.", - default_value = "80" - )] - http_port: u16, -} - -fn main() -> Result<(), Box> { - match Subcommand::parse() { - Subcommand::Decipher => { - let transaction = Transaction::consensus_decode(&mut io::stdin()) - .map_err(|err| format!("Failed to decode transaction: {err}"))?; - - let message = Runestone::decipher(&transaction)?; - - serde_json::to_writer_pretty(&io::stdout(), &message)?; - println!(); - } - Subcommand::Server(server) => Runtime::new()?.block_on(async { - let addr = ("0.0.0.0", server.http_port) - .to_socket_addrs()? - .next() - .unwrap(); - - axum_server::Server::bind(addr) - .serve(Router::new().route("/", get(home)).into_make_service()) - .await - })?, - } - - Ok(()) -} - -async fn home() -> &'static str { - "Hello, world!" -} diff --git a/src/runestone.rs b/src/runestone.rs deleted file mode 100644 index c9213e7..0000000 --- a/src/runestone.rs +++ /dev/null @@ -1,398 +0,0 @@ -use super::*; - -#[derive(Default, Serialize, Debug, PartialEq)] -pub struct Runestone { - pub directives: Vec, - pub decimals: Option, - pub symbol: Option, -} - -impl Runestone { - pub fn decipher(transaction: &Transaction) -> Result> { - let Some(payload) = Runestone::payload(transaction)? else { - return Ok(None); - }; - - let mut integers = Vec::new(); - let mut i = 0; - - while i < payload.len() { - let (integer, length) = varint::decode(&payload[i..])?; - integers.push(integer); - i += length; - } - - let mut directives = Vec::new(); - let mut decimals = None; - let mut symbol = None; - - for chunk in integers.chunks(3) { - match chunk { - [id, amount, output] => directives.push(Directive { - id: *id, - amount: *amount, - output: *output, - }), - [d] => decimals = Some(*d), - [d, s] => { - decimals = Some(*d); - symbol = Some(*s); - } - _ => unreachable!(), - } - } - - Ok(Some(Self { - directives, - decimals, - symbol, - })) - } - - fn payload(transaction: &Transaction) -> Result>> { - for output in &transaction.output { - let mut instructions = output.script_pubkey.instructions(); - - if instructions.next().transpose()? != Some(Instruction::Op(opcodes::all::OP_RETURN)) { - continue; - } - - if instructions.next().transpose()? != Some(Instruction::PushBytes(b"RUNE_TEST".into())) { - continue; - } - - let mut payload = Vec::new(); - - for result in instructions { - match result? { - Instruction::PushBytes(push) => payload.extend_from_slice(push.as_bytes()), - Instruction::Op(op) => return Err(Error::Opcode(op)), - } - } - - return Ok(Some(payload)); - } - - Ok(None) - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - bitcoin::{locktime, script::PushBytes, ScriptBuf, TxOut}, - }; - - #[test] - fn deciphering_transaction_with_no_outputs_returns_none() { - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: Vec::new(), - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(None) - ); - } - - #[test] - fn deciphering_transaction_with_non_op_return_output_returns_none() { - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new().push_slice([]).into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(None) - ); - } - - #[test] - fn deciphering_transaction_with_bare_op_return_returns_none() { - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(None) - ); - } - - #[test] - fn deciphering_transaction_with_non_matching_op_return_returns_none() { - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"FOOO") - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(None) - ); - } - - #[test] - fn deciphering_valid_runestone_with_invalid_script_returns_script_error() { - let result = Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: ScriptBuf::from_bytes(vec![opcodes::all::OP_PUSHBYTES_4.to_u8()]), - value: 0, - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }); - - match result { - Ok(_) => panic!("expected error"), - Err(Error::Script(_)) => {} - Err(err) => panic!("unexpected error: {err}"), - } - } - - #[test] - fn deciphering_valid_runestone_with_invalid_script_postfix_returns_script_error() { - let mut script_pubkey = script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .into_script() - .into_bytes(); - - script_pubkey.push(opcodes::all::OP_PUSHBYTES_4.to_u8()); - - let result = Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: ScriptBuf::from_bytes(script_pubkey), - value: 0, - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }); - - match result { - Ok(_) => panic!("expected error"), - Err(Error::Script(_)) => {} - Err(err) => panic!("unexpected error: {err}"), - } - } - - #[test] - fn deciphering_empty_runestone_is_successful() { - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { - directives: Vec::new(), - decimals: None, - symbol: None, - })) - ); - } - - fn payload(integers: &[u128]) -> Vec { - let mut payload = Vec::new(); - - for integer in integers { - payload.extend(varint::encode(*integer)); - } - - payload - } - - #[test] - fn deciphering_non_empty_runestone_is_successful() { - let payload = payload(&[1, 2, 3]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { - directives: vec![Directive { - id: 1, - amount: 2, - output: 3, - }], - decimals: None, - symbol: None, - })) - ); - } - - #[test] - fn additional_integer_is_decimals() { - let payload = payload(&[1, 2, 3, 4]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { - directives: vec![Directive { - id: 1, - amount: 2, - output: 3, - }], - decimals: Some(4), - symbol: None, - })) - ); - } - - #[test] - fn additional_two_integers_are_decimals_and_symbol() { - let payload = payload(&[1, 2, 3, 4, 5]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { - directives: vec![Directive { - id: 1, - amount: 2, - output: 3, - }], - decimals: Some(4), - symbol: Some(5), - })) - ); - } - - #[test] - fn runestone_may_contain_multipe_directives() { - let payload = payload(&[1, 2, 3, 4, 5, 6]); - - let payload: &PushBytes = payload.as_slice().try_into().unwrap(); - - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice(payload) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { - directives: vec![ - Directive { - id: 1, - amount: 2, - output: 3, - }, - Directive { - id: 4, - amount: 5, - output: 6, - }, - ], - decimals: None, - symbol: None, - })) - ); - } - - #[test] - fn payload_pushes_are_concatinated() { - assert_eq!( - Runestone::decipher(&Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .push_slice::<&PushBytes>(varint::encode(1).as_slice().try_into().unwrap()) - .push_slice::<&PushBytes>(varint::encode(2).as_slice().try_into().unwrap()) - .push_slice::<&PushBytes>(varint::encode(3).as_slice().try_into().unwrap()) - .push_slice::<&PushBytes>(varint::encode(4).as_slice().try_into().unwrap()) - .push_slice::<&PushBytes>(varint::encode(5).as_slice().try_into().unwrap()) - .into_script(), - value: 0 - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }), - Ok(Some(Runestone { - directives: vec![Directive { - id: 1, - amount: 2, - output: 3, - }], - decimals: Some(4), - symbol: Some(5), - })) - ); - } -} diff --git a/src/varint.rs b/src/varint.rs deleted file mode 100644 index 988b219..0000000 --- a/src/varint.rs +++ /dev/null @@ -1,110 +0,0 @@ -use super::*; - -#[cfg(test)] -pub fn encode(mut n: u128) -> Vec { - let mut out = Vec::new(); - - loop { - let mut byte = n as u8 % 128; - - if !out.is_empty() { - byte |= 0b1000_0000; - } - - out.push(byte); - - if n < 128 { - break; - } - - n = n / 128 - 1; - } - - out.reverse(); - - out -} - -pub fn decode(buffer: &[u8]) -> Result<(u128, usize)> { - let mut n = 0; - let mut i = 0; - - loop { - let b = buffer.get(i).cloned().unwrap() as u128; - - if b < 128 { - return Ok((n + b, i + 1)); - } - - n += b - 127; - - n = n.checked_mul(128).ok_or(Error::Varint)?; - - i += 1; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn powers_of_two_round_trip_successfully() { - for i in 0..128 { - let n = 1 << i; - let encoded = encode(n); - let (decoded, length) = decode(&encoded).unwrap(); - assert_eq!(decoded, n); - assert_eq!(length, encoded.len()); - } - } - - #[test] - fn alternating_bit_strings_round_trip_successfully() { - let mut n = 0; - - for i in 0..129 { - n = n << 1 | (i % 2); - let encoded = encode(n); - let (decoded, length) = decode(&encoded).unwrap(); - assert_eq!(decoded, n); - assert_eq!(length, encoded.len()); - } - } - - #[test] - fn decoding_integer_over_max_is_an_error() { - assert_eq!( - decode(&[ - 130, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 255, - 0, - ]), - Err(Error::Varint) - ); - } - - #[test] - fn taproot_annex_format_bip_test_vectors_round_trip_successfully() { - const TEST_VECTORS: &[(u128, &[u8])] = &[ - (0, &[0x00]), - (1, &[0x01]), - (127, &[0x7F]), - (128, &[0x80, 0x00]), - (255, &[0x80, 0x7F]), - (256, &[0x81, 0x00]), - (16383, &[0xFE, 0x7F]), - (16384, &[0xFF, 0x00]), - (16511, &[0xFF, 0x7F]), - (65535, &[0x82, 0xFE, 0x7F]), - (1 << 32, &[0x8E, 0xFE, 0xFE, 0xFF, 0x00]), - ]; - - for (n, encoding) in TEST_VECTORS { - let actual = encode(*n); - assert_eq!(actual, *encoding); - let (actual, length) = decode(encoding).unwrap(); - assert_eq!(actual, *n); - assert_eq!(length, encoding.len()); - } - } -} diff --git a/tests/decipher.rs b/tests/decipher.rs deleted file mode 100644 index 9e8b853..0000000 --- a/tests/decipher.rs +++ /dev/null @@ -1,79 +0,0 @@ -use super::*; - -#[test] -fn transaction_with_no_runestone_returns_null() { - let child = Command::new(executable_path("runestone")) - .arg("decipher") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap(); - - let transaction = Transaction { - input: Vec::new(), - output: Vec::new(), - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }; - - let mut buffer = Vec::new(); - - transaction.consensus_encode(&mut buffer).unwrap(); - - child.stdin.as_ref().unwrap().write_all(&buffer).unwrap(); - - let output = child.wait_with_output().unwrap(); - - assert!(output.status.success()); - - let stdout = str::from_utf8(&output.stdout).unwrap(); - - assert_eq!(stdout, "null\n"); -} - -#[test] -fn transaction_with_runestone_returns_serialized_runestone() { - let child = Command::new(executable_path("runestone")) - .arg("decipher") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap(); - - let transaction = Transaction { - input: Vec::new(), - output: vec![TxOut { - script_pubkey: script::Builder::new() - .push_opcode(opcodes::all::OP_RETURN) - .push_slice(b"RUNE_TEST") - .into_script(), - value: 0, - }], - lock_time: locktime::absolute::LockTime::ZERO, - version: 0, - }; - - let mut buffer = Vec::new(); - - transaction.consensus_encode(&mut buffer).unwrap(); - - child.stdin.as_ref().unwrap().write_all(&buffer).unwrap(); - - let output = child.wait_with_output().unwrap(); - - assert!(output.status.success()); - - let stdout = str::from_utf8(&output.stdout).unwrap(); - - assert_eq!( - stdout, - r#"{ - "directives": [], - "decimals": null, - "symbol": null -} -"# - ); -} diff --git a/tests/lib.rs b/tests/lib.rs deleted file mode 100644 index 39adcc9..0000000 --- a/tests/lib.rs +++ /dev/null @@ -1,15 +0,0 @@ -use { - bitcoin::{consensus::Encodable, locktime, opcodes, script, Transaction, TxOut}, - executable_path::executable_path, - reqwest::blocking as reqwest, - std::{ - io::Write, - net::TcpListener, - process::{self, Command, Stdio}, - str, thread, - time::Duration, - }, -}; - -mod decipher; -mod server; diff --git a/tests/server.rs b/tests/server.rs deleted file mode 100644 index e71941d..0000000 --- a/tests/server.rs +++ /dev/null @@ -1,45 +0,0 @@ -use super::*; - -struct KillOnDrop(process::Child); - -impl Drop for KillOnDrop { - fn drop(&mut self) { - self.0.kill().unwrap() - } -} - -#[test] -fn server_returns_homepage() { - let port = TcpListener::bind("127.0.0.1:0") - .unwrap() - .local_addr() - .unwrap() - .port(); - - let _server = KillOnDrop( - Command::new(executable_path("runestone")) - .args(["server", "--http-port", &port.to_string()]) - .spawn() - .unwrap(), - ); - - for i in 0..100 { - if reqwest::get(format!("http://localhost:{port}")).is_ok() { - break; - } - - if i == 99 { - panic!("server failed to start"); - } - - thread::sleep(Duration::from_millis(100)); - } - - assert_eq!( - reqwest::get(format!("http://localhost:{port}")) - .unwrap() - .text() - .unwrap(), - "Hello, world!" - ); -}