diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6778fb9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: Rust Chat CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build_and_test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + + - name: Build + run: cargo build --verbose + + - name: Run Integration Test + run: | + cargo run --bin server > server.log 2>&1 & + SERVER_PID=$! + + echo "Server started with PID $SERVER_PID. Waiting for it to initialize..." + sleep 5 + + # 2. Run the Client with piped input + echo -e "send Hello GitHub Actions\nleave" | cargo run --bin client 127.0.0.1 8080 Abhi + + CLIENT_EXIT_CODE=$? + + kill $SERVER_PID + + if [ $CLIENT_EXIT_CODE -ne 0 ]; then + echo "Client failed!" + cat server.log + exit 1 + fi + + echo "Integration test passed!" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6985cf1..280b474 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,14 @@ # Generated by Cargo # will have compiled files and executables debug/ -target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + + +# Added by cargo + +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c4d7bcc --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,586 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + +[[package]] +name = "simple-chat" +version = "0.1.0" +dependencies = [ + "chrono", + "colored", + "tokio", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..21ab578 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "simple-chat" +version = "0.1.0" +edition = "2024" + +[dependencies] +chrono = "0.4.42" +colored = "3.0.0" +tokio = { version = "1.48.0", features = ["full"] } diff --git a/README.md b/README.md index 8c4d4e1..afc832b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,45 @@ # Simple Chat +## Project Structure + +* `src/bin/server.rs`: The chat server implementation. +* `src/bin/client.rs`: The CLI client implementation. + +## How to Run + +### Prerequisites +Ensure you have Rust and Cargo installed: +```bash +curl --proto '=https' --tlsv1.2 -sSf [https://sh.rustup.rs](https://sh.rustup.rs) | sh +``` + +### 1. Start the Server +The server listens on 127.0.0.1:8080 by default. + +```bash + +cargo run --bin server +``` + +### 2. Start a Client +Open a new terminal window to connect a client. You must provide a username. + +```bash + +cargo run --bin client 127.0.0.1 8080 Abhi +``` + +Commands inside the client: + +Type send to broadcast a message. + +Type leave to disconnect and exit. + + +```bash +cargo run --bin client 127.0.0.1 8080 Molly +``` + ## Summary You have been tasked with writing a simple asynchronous chat server and CLI diff --git a/src/bin/client.rs b/src/bin/client.rs new file mode 100644 index 0000000..6427177 --- /dev/null +++ b/src/bin/client.rs @@ -0,0 +1,111 @@ +use chrono::Local; +use colored::*; +use std::env; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::TcpStream; + +const MOVE_CURSOR_UP: &str = "\x1b[1A"; +const CLEAR_LINE: &str = "\x1b[2K"; +const CARRIAGE_RETURN: &str = "\r"; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args: Vec = env::args().collect(); + if args.len() < 4 { + eprintln!("{} client ", "Usage:".red().bold()); + return Ok(()); + } + let (host, port, username) = (&args[1], &args[2], &args[3]); + let addr = format!("{}:{}", host, port); + + print!("\x1b[2J\x1b[1;1H"); + println!(" {} {}", "Connected to:".green(), addr.yellow()); + println!(" {} {}\n", " Username:".green(), username.yellow()); + println!( + "{}\n", + "Type 'send ' to chat, 'leave' to quit." + .dimmed() + .italic() + ); + + let mut stream = TcpStream::connect(&addr).await?; + stream + .write_all(format!("{}\n", username).as_bytes()) + .await?; + + let (reader, mut writer) = stream.into_split(); + + tokio::spawn(async move { + let mut reader = BufReader::new(reader); + let mut line = String::new(); + + loop { + match reader.read_line(&mut line).await { + Ok(0) => { + println!("\n{}", "[Server closed connection]".red().bold()); + std::process::exit(0); + } + Ok(_) => { + let text = line.trim_end(); + + let time = Local::now().format("%H:%M").to_string(); + let timestamp = format!("[{}]", time).dimmed(); + + print!("{}{}", CARRIAGE_RETURN, CLEAR_LINE); + + if let Some((name, msg)) = text.split_once(": ") { + println!("{} {}: {}", timestamp, name.bold().cyan(), msg); + } else { + println!("{} {}", timestamp, text.italic()); + } + + print!("{}", "> ".green().bold()); + use std::io::Write; + let _ = std::io::stdout().flush(); + + line.clear(); + } + Err(_) => { + std::process::exit(1); + } + } + } + }); + + let mut stdin = BufReader::new(tokio::io::stdin()); + let mut stdout = tokio::io::stdout(); + let mut line = String::new(); + + loop { + stdout + .write_all(format!("{}", "> ".green().bold()).as_bytes()) + .await?; + stdout.flush().await?; + + line.clear(); + let bytes_read = stdin.read_line(&mut line).await?; + if bytes_read == 0 { + break; + } + + let input = line.trim(); + + if input == "leave" { + println!("{}", "Leaving chat...".yellow()); + break; + } else if let Some(msg) = input.strip_prefix("send ") { + writer.write_all(format!("{}\n", msg).as_bytes()).await?; + + let time = Local::now().format("%H:%M").to_string(); + let timestamp = format!("[{}]", time).dimmed(); + + print!("{}{}{}", MOVE_CURSOR_UP, CLEAR_LINE, CARRIAGE_RETURN); + + println!("{} {}: {}", timestamp, "You".bold().green(), msg); + } else { + println!("{}", "Invalid command. Use 'send ' or 'leave'".red()); + } + } + + Ok(()) +} diff --git a/src/bin/server.rs b/src/bin/server.rs new file mode 100644 index 0000000..315b94b --- /dev/null +++ b/src/bin/server.rs @@ -0,0 +1,97 @@ +use std::collections::HashSet; +use std::sync::{Arc, Mutex}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::TcpListener; +use tokio::sync::broadcast; + +type InMemoryUserDb = Arc>>; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let listener = TcpListener::bind("127.0.0.1:8080").await?; + println!("Server running on 127.0.0.1:8080"); + + let (tx, _rx) = broadcast::channel::<(String, String)>(100); + let state: InMemoryUserDb = InMemoryUserDb::new(Mutex::new(HashSet::new())); + + loop { + let (mut socket, addr) = listener.accept().await?; + let tx = tx.clone(); + let mut rx = tx.subscribe(); + let state = state.clone(); + + tokio::spawn(async move { + let (reader, mut writer) = socket.split(); + let mut reader = BufReader::new(reader); + let mut line = String::new(); + + let username = match reader.read_line(&mut line).await { + Ok(n) if n > 0 => { + let name = line.trim().to_string(); + line.clear(); + name + } + _ => return, + }; + + let is_taken = { + let mut users = state.lock().unwrap(); + if users.contains(&username) { + true + } else { + users.insert(username.clone()); + false + } + }; + + if is_taken { + let _ = writer.write_all(b"Error: Username taken\n").await; + return; + } + + println!("User '{}' joined from {:?}", username, addr); + let _ = writer.write_all(b"Welcome to the chat!\n").await; + + let _ = tx.send(( + username.clone(), + format!("{} has joined the chat", username), + )); + + loop { + tokio::select! { + result = reader.read_line(&mut line) => { + match result { + Ok(0) => break, + Ok(_) => { + let msg = line.trim(); + if !msg.is_empty() { + let _ = tx.send((username.clone(), format!("{}: {}", username, msg))); + } + line.clear(); + } + Err(_) => break, + } + } + result = rx.recv() => { + match result { + Ok((sender_name, msg)) => { + if sender_name != username { + if writer.write_all(msg.as_bytes()).await.is_err() { break; } + let _ = writer.write_all(b"\n").await; + } + } + Err(_) => break, + } + } + } + } + + { + let mut users = state.lock().unwrap(); + users.remove(&username); + } + let _ = tx.send((username.clone(), format!("{} has left the chat", username))); + println!("User '{}' disconnected", username); + }); + } +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100644 index 0000000..c20145c --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,86 @@ +use std::process::{Child, Command, Stdio}; +use std::time::{Duration, Instant}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::TcpStream; +use tokio::time::sleep; + +const SERVER_ADDR: &str = "127.0.0.1:8080"; + +async fn start_server() -> Child { + let status = Command::new("cargo") + .args(&["build", "--bin", "server"]) + .status() + .expect("Failed to build server"); + assert!(status.success(), "Server binary failed to build"); + + let child = Command::new("cargo") + .args(&["run", "--bin", "server"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("Failed to start server process"); + + let start = Instant::now(); + loop { + if TcpStream::connect(SERVER_ADDR).await.is_ok() { + break; + } + if start.elapsed() > Duration::from_secs(5) { + panic!( + "Server timed out: Could not connect to {} after 5s", + SERVER_ADDR + ); + } + sleep(Duration::from_millis(100)).await; + } + + child +} + +#[tokio::test] +async fn test_server_client_interaction() { + let mut server_process = start_server().await; + + let mut stream_a = TcpStream::connect(SERVER_ADDR).await.unwrap(); + let (read_a, mut write_a) = stream_a.split(); + let mut _reader_a = BufReader::new(read_a); + + let mut stream_b = TcpStream::connect(SERVER_ADDR).await.unwrap(); + let (read_b, mut write_b) = stream_b.split(); + let mut reader_b = BufReader::new(read_b); + + write_a.write_all(b"Alice\n").await.unwrap(); + write_b.write_all(b"Bob\n").await.unwrap(); + + sleep(Duration::from_millis(100)).await; + + let msg = "Integration Test Message"; + write_a + .write_all(format!("{}\n", msg).as_bytes()) + .await + .unwrap(); + + let mut line = String::new(); + let mut found = false; + + for _ in 0..5 { + line.clear(); + let read_result = + tokio::time::timeout(Duration::from_secs(1), reader_b.read_line(&mut line)).await; + + if let Ok(Ok(bytes)) = read_result { + if bytes == 0 { + break; + } + if line.contains(msg) { + assert!(line.contains("Alice"), "Message MUST contain username"); + found = true; + break; + } + } else { + break; + } + } + assert!(found, "Bob never received Alice's message!"); + server_process.kill().expect("Failed to kill server"); +} diff --git a/tests/load_test.rs b/tests/load_test.rs new file mode 100644 index 0000000..26a9df2 --- /dev/null +++ b/tests/load_test.rs @@ -0,0 +1,76 @@ +use std::process::{Child, Command, Stdio}; +use std::time::{Duration, Instant}; +use tokio::io::AsyncWriteExt; +use tokio::net::TcpStream; +use tokio::time::sleep; + +const SERVER_ADDR: &str = "127.0.0.1:8080"; +const CLIENT_COUNT: usize = 10000; + +async fn start_server() -> Child { + let _ = Command::new("cargo") + .args(&["build", "--bin", "server"]) + .status(); + let child = Command::new("cargo") + .args(&["run", "--bin", "server"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .expect("Failed to start server"); + + let start = Instant::now(); + loop { + if TcpStream::connect(SERVER_ADDR).await.is_ok() { + break; + } + if start.elapsed() > Duration::from_secs(5) { + panic!("Server timeout"); + } + sleep(Duration::from_millis(100)).await; + } + child +} + +#[tokio::test] +async fn test_high_concurrency_load() { + let mut server_process = start_server().await; + + let mut handles = vec![]; + + println!("Starting load test with {} clients...", CLIENT_COUNT); + + for i in 0..CLIENT_COUNT { + handles.push(tokio::spawn(async move { + match TcpStream::connect(SERVER_ADDR).await { + Ok(mut stream) => { + let username = format!("Bot{}", i); + if let Err(_) = stream.write_all(format!("{}\n", username).as_bytes()).await { + return false; + } + sleep(Duration::from_millis(500)).await; + true + } + Err(_) => false, + } + })); + } + + let mut success_count = 0; + for handle in handles { + if handle.await.unwrap_or(false) { + success_count += 1; + } + } + + server_process.kill().expect("Failed to kill server"); + + println!( + "Load Test Result: {}/{} clients connected successfully.", + success_count, CLIENT_COUNT + ); + + assert_eq!( + success_count, CLIENT_COUNT, + "Server dropped connections under load!" + ); +}