diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5d20a448..bb88e215 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,38 +2,60 @@ name: Rust on: push: - branches: [ "main" ] - paths: ['src/**', 'tests/**', 'third-party/**', 'Cargo.toml', 'build.rs', 'init.sh', '.github/workflows/rust.yml'] + branches: ["main"] + paths: + [ + "src/**", + "tests/**", + "third-party/**", + "Cargo.toml", + "build.rs", + "init.sh", + ".github/workflows/rust.yml", + ] pull_request: - branches: [ "main" ] - paths: ['src/**', 'tests/**', 'third-party/**', 'Cargo.toml', 'build.rs', 'init.sh', '.github/workflows/rust.yml'] + branches: ["main"] + paths: + [ + "src/**", + "tests/**", + "third-party/**", + "Cargo.toml", + "build.rs", + "init.sh", + ".github/workflows/rust.yml", + ] env: CARGO_TERM_COLOR: always jobs: build: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 - with: - submodules: 'true' + - uses: actions/checkout@v6 + with: + submodules: "true" + + # Step to install OCaml compiler and dependencies + - name: Install OCaml + run: | + sudo apt-get update + sudo apt-get install -y ocaml opam + opam init --disable-sandboxing -y + eval $(opam env) + - name: Install watchman + run: | + curl -L --output watchman.deb https://github.com/mszabo-wikia/watchman/releases/download/v0.1.3-testing/watchman_ubuntu24.04_v0.1.3-testing.deb + echo "d88fa6b2b5040b6cf3f3c26b3c0eb21d4273c9591205657bb4363b83f8483944 watchman.deb" | sha256sum -c + sudo apt install -y ./watchman.deb - # Step to install OCaml compiler and dependencies - - name: Install OCaml - run: | - sudo apt-get update - sudo apt-get install -y ocaml opam watchman - opam init --disable-sandboxing -y - eval $(opam env) - - - name: Init repository - run: ./init.sh - - name: Build & Run tests - run: | - cargo run --release --bin hakana test tests + - name: Init repository + run: ./init.sh + - name: Build & Run tests + run: | + cargo run --release --bin hakana test tests - # run Rust unit tests - cargo test --release --workspace + # run Rust unit tests + cargo test --release --workspace diff --git a/src/cli/lib.rs b/src/cli/lib.rs index 1091d59a..c4ba9c87 100644 --- a/src/cli/lib.rs +++ b/src/cli/lib.rs @@ -636,6 +636,13 @@ pub async fn init( Logger::DevNull } } + Some(("server", sub_matches)) => { + if sub_matches.is_present("debug") { + Logger::CommandLine(Verbosity::Debugging) + } else { + Logger::CommandLine(Verbosity::Simple) + } + } Some((_, sub_matches)) => { if sub_matches.is_present("debug") { Logger::CommandLine(Verbosity::Debugging) @@ -2714,6 +2721,7 @@ async fn do_server( config_path: Some(config_path), plugins, header: header.to_string(), + chaos_monkey: None, }; match Server::new(server_config, Arc::new(logger)) { diff --git a/src/language_server/server_client.rs b/src/language_server/server_client.rs index af12fc29..0c526e57 100644 --- a/src/language_server/server_client.rs +++ b/src/language_server/server_client.rs @@ -98,11 +98,8 @@ impl ServerConnection { ); let log_path = std::env::temp_dir().join("hakana-server.log"); - let log_file = std::fs::File::create(&log_path).ok(); - let log_file2 = std::fs::OpenOptions::new() - .append(true) - .open(&log_path) - .ok(); + let stdout = std::fs::File::create(&log_path)?; + let stderr = stdout.try_clone()?; eprintln!("Server log file: {}", log_path.display()); @@ -112,8 +109,8 @@ impl ServerConnection { .arg(project_root) .current_dir(project_root) .stdin(Stdio::null()) - .stdout(log_file.map(Stdio::from).unwrap_or(Stdio::null())) - .stderr(log_file2.map(Stdio::from).unwrap_or(Stdio::null())) + .stdout(Stdio::from(stdout)) + .stderr(Stdio::from(stderr)) .spawn() .map_err(|e| { io::Error::new( diff --git a/src/server/handler.rs b/src/server/handler.rs index 7cb909ac..51e7012f 100644 --- a/src/server/handler.rs +++ b/src/server/handler.rs @@ -1,22 +1,18 @@ //! Request handlers for the hakana server. use crate::{ServerConfig, ServerState}; -use hakana_analyzer::config::Config; -use hakana_code_info::analysis_result::{self, AnalysisResult}; -use hakana_code_info::data_flow::graph::{GraphKind, WholeProgramKind}; +use hakana_code_info::analysis_result::AnalysisResult; use hakana_logger::Logger; use hakana_orchestrator::SuccessfulScanData; use hakana_orchestrator::file::FileStatus; use hakana_protocol::{ - AckResponse, AnalyzeRequest, AnalyzeResponse, ErrorCode, ErrorResponse, FileChange, + AckResponse, FindReferencesRequest, FindReferencesResponse, FindSymbolReferencesRequest, FindSymbolReferencesResponse, GotoDefinitionRequest, GotoDefinitionResponse, Message, - ProtocolIssue, ReferenceLocation, SecurityCheckRequest, SecurityCheckResponse, StatusResponse, + ProtocolIssue, ReferenceLocation, StatusResponse, }; -use hakana_protocol::{FileChangeStatus, GetIssuesResponse}; -use hakana_str::Interner; -use rustc_hash::{FxHashMap, FxHashSet}; -use std::path::Path; +use hakana_protocol::GetIssuesResponse; +use rustc_hash::FxHashMap; use std::sync::{Arc, Mutex}; use std::time::Instant; use tokio::sync::mpsc::Sender; diff --git a/src/server/lib.rs b/src/server/lib.rs index a6a367b5..e174756f 100644 --- a/src/server/lib.rs +++ b/src/server/lib.rs @@ -29,6 +29,7 @@ pub struct ServerConfig { pub config_path: Option, pub plugins: Vec>, pub header: String, + pub chaos_monkey: Option>, } impl ServerConfig { @@ -39,6 +40,7 @@ impl ServerConfig { config_path: None, plugins: Vec::new(), header: String::new(), + chaos_monkey: None, } } } @@ -443,8 +445,126 @@ fn run_analysis( previous_scan_data, previous_analysis_result, changes, - || {}, + || { + if let Some(f) = &config.chaos_monkey { + f(); + } + }, Some(progress), ) .map_err(|e| e.to_string()) } + +#[cfg(test)] +mod tests { + use hakana_logger::Logger; + use hakana_protocol::{ClientSocket, GetIssuesRequest, Message}; + use std::{ + path::{Path, PathBuf}, + sync::{Arc, atomic::AtomicBool}, + }; + use tokio::fs; + + use crate::{Server, ServerConfig, watchman}; + + #[tokio::test] + async fn handles_file_changes_during_analysis() -> std::io::Result<()> { + let tmp = tempfile::Builder::new() + .prefix("hakana-test") + .tempdir() + .expect("failed to create temp dir"); + let hack_file = tmp.path().join("index.hack"); + let config_path = tmp.path().join("hakana.json"); + fs::write(hack_file.clone(), "function main(): void {}") + .await + .expect("failed to create test file"); + fs::write(config_path.clone(), "{}") + .await + .expect("failed to create test file"); + + eprintln!("{:?}", hack_file); + + let did_mutate = AtomicBool::new(false); + + let server_config = ServerConfig { + root_dir: tmp.path().to_str().unwrap().to_string(), + threads: 2, + config_path: Some(config_path.to_str().unwrap().to_string()), + plugins: vec![], + header: "".to_string(), + chaos_monkey: Some(Arc::new(move || { + // Simulate a file change between the first scan and first analysis + if !did_mutate.load(std::sync::atomic::Ordering::Relaxed) { + eprintln!("editing file before analysis"); + did_mutate.store(true, std::sync::atomic::Ordering::Relaxed); + std::fs::write(hack_file.clone(), "function foo(): void {}") + .expect("failed to mutate file before analysis"); + } + })), + }; + + let watchman_clock = watchman::get_clock(Path::new(&server_config.root_dir)).await?; + let mut watchman_handle = watchman::start_subscription( + PathBuf::from(&server_config.root_dir), + vec![], + watchman_clock, + server_config.config_path.as_ref().map(&PathBuf::from), + ); + + let mut server = Server::new( + server_config, + Arc::new(Logger::CommandLine(hakana_logger::Verbosity::Debugging)), + ) + .expect("failed to create server"); + + let socket_path = server.socket_path().clone(); + let shutdown_tx = server.shutdown_tx.clone(); + + let server_task = + tokio::spawn(async move { server.run().await.expect("failed to run server") }); + + // Wait for Watchman to send a notification (which should also have notified the server) + watchman_handle.recv().await; + + let mut client = ClientSocket::connect(&socket_path) + .await + .expect("failed to connect"); + + let request = Message::GetIssues(GetIssuesRequest { + filter: None, + find_unused_expressions: false, + find_unused_definitions: false, + block_until_next_analysis: false, + send_progress_report: false, + }); + + let response = client + .request(&request) + .await + .expect("Failed to send request"); + + if let Message::GetIssuesResult(result) = response { + eprintln!("{:?}", result.issues); + assert!( + result.analysis_complete, + "Analysis should be complete after server is ready" + ); + assert_eq!(0, result.issues.len(), "there should be no issues reported"); + assert_eq!( + 1, result.files_analyzed, + "one file should have been analyzed" + ) + } else { + panic!("Expected GetIssuesResult, got {:?}", response); + } + + shutdown_tx + .send(true) + .await + .expect("failed to shutdown server"); + + server_task.await.expect("failed to join server task"); + + Ok(()) + } +}