590 lines
20 KiB
Rust
590 lines
20 KiB
Rust
//! Runtime verification and safe fallback for JVM (GC) arguments.
|
|
//!
|
|
//! The frontend picks the *preferred* GC strategy from machine heuristics and
|
|
//! sends an ordered list of candidate argument blocks. This module verifies
|
|
//! each candidate against the *actual* JVM binary that will launch Minecraft,
|
|
//! prunes only the unsupported *tuning* flags (never the GC selector, which
|
|
//! defines the strategy), and falls back down the candidate chain — ending at
|
|
//! launching with no GC arguments at all rather than refusing to start.
|
|
use regex::Regex;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::future::Future;
|
|
use std::path::Path;
|
|
use std::pin::Pin;
|
|
use std::sync::{Mutex, OnceLock};
|
|
use std::time::Duration;
|
|
use tokio::process::Command;
|
|
use tokio::time::timeout;
|
|
use tracing::warn;
|
|
|
|
/// Marker token the frontend persists to mean "auto-select GC".
|
|
pub const AUTO_GC_PRESET_ARG: &str = "@axolotl:gc:auto";
|
|
|
|
/// An `-XX:` flag whose key starts with `+Use` is a collector selector
|
|
/// (`UseG1GC`, `UseShenandoahGC`, `UseZGC`). Dropping it silently changes the
|
|
/// collector, so we never prune it — an unsupported selector means the whole
|
|
/// strategy is unusable on this JVM.
|
|
fn is_gc_selector(arg: &str) -> bool {
|
|
arg.starts_with("-XX:+Use")
|
|
}
|
|
|
|
/// Frontend → backend intent describing which GC preset is active and the
|
|
/// ordered candidates to try.
|
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
pub struct GcLaunchIntent {
|
|
/// Frontend preset id (`gc-auto`, `gc-g1gc-mojang`, `gc-g1gc-pcl`,
|
|
/// `gc-shenandoah` or `gc-zgc`).
|
|
pub active_preset_id: String,
|
|
/// Exact tokens currently present in the effective java args that belong
|
|
/// to this preset — or just the `@axolotl:gc:auto` marker for auto.
|
|
pub block_tokens: Vec<String>,
|
|
/// Strategy id for each candidate (`zgc`, `shenandoah`, `g1gc-mojang`,
|
|
/// `g1gc-pcl`, `minimal-g1`). Parallel to `candidates`, `[0]` preferred.
|
|
pub candidate_ids: Vec<String>,
|
|
/// Parallel to `candidate_ids`: the ordered candidate argument blocks.
|
|
pub candidates: Vec<Vec<String>>,
|
|
}
|
|
|
|
impl GcLaunchIntent {
|
|
pub fn preferred_id(&self) -> String {
|
|
self.candidate_ids
|
|
.first()
|
|
.cloned()
|
|
.unwrap_or_else(|| self.active_preset_id.clone())
|
|
}
|
|
}
|
|
|
|
/// Result of verification/fallback, returned to the frontend so it can tell
|
|
/// the user what the JVM actually accepted.
|
|
#[derive(Clone, Debug, Serialize)]
|
|
pub struct GcLaunchReport {
|
|
pub preferred_strategy: String,
|
|
pub chosen_strategy: String,
|
|
pub chosen_args: Vec<String>,
|
|
pub pruned_args: Vec<String>,
|
|
pub reason_chain: Vec<String>,
|
|
}
|
|
|
|
impl GcLaunchReport {
|
|
/// Whether the launch deviated from the preferred resolution (strategy
|
|
/// fallback or flag-level pruning).
|
|
pub fn fell_back(&self) -> bool {
|
|
self.chosen_strategy != self.preferred_strategy
|
|
|| !self.pruned_args.is_empty()
|
|
}
|
|
}
|
|
|
|
struct ProbeOutcome {
|
|
supported: bool,
|
|
stderr_text: String,
|
|
}
|
|
|
|
/// Whether a particular (java, args) set already accepted/succeeded.
|
|
fn probe_cache() -> &'static Mutex<HashMap<(String, Vec<String>), bool>> {
|
|
static CACHE: OnceLock<Mutex<HashMap<(String, Vec<String>), bool>>> =
|
|
OnceLock::new();
|
|
CACHE.get_or_init(|| Mutex::new(HashMap::new()))
|
|
}
|
|
|
|
/// Run `java <args> -version` and report whether the JVM accepted `args`.
|
|
/// A supported flag set exits 0; an unrecognized option exits non-zero with
|
|
/// an explanatory message on stderr.
|
|
async fn probe_jvm_arguments(java: &Path, args: &[String]) -> ProbeOutcome {
|
|
let key = (java.to_string_lossy().into_owned(), args.to_vec());
|
|
if let Some(supported) = probe_cache().lock().unwrap().get(&key).copied() {
|
|
return ProbeOutcome {
|
|
supported,
|
|
stderr_text: String::new(),
|
|
};
|
|
}
|
|
|
|
let timed = timeout(
|
|
Duration::from_secs(5),
|
|
Command::new(java)
|
|
.args(args)
|
|
.arg("-version")
|
|
.env_remove("_JAVA_OPTIONS")
|
|
.kill_on_drop(true)
|
|
.output(),
|
|
)
|
|
.await;
|
|
|
|
let outcome = match timed {
|
|
Ok(Ok(output)) => {
|
|
let mut text = String::from_utf8_lossy(&output.stderr).into_owned();
|
|
if !output.stdout.is_empty() {
|
|
text.push_str(&String::from_utf8_lossy(&output.stdout));
|
|
}
|
|
ProbeOutcome {
|
|
supported: output.status.success(),
|
|
stderr_text: text,
|
|
}
|
|
}
|
|
// Spawn failure or timeout: treat as unsupported and move on.
|
|
Ok(Err(_)) | Err(_) => ProbeOutcome {
|
|
supported: false,
|
|
stderr_text: String::new(),
|
|
},
|
|
};
|
|
|
|
probe_cache().lock().unwrap().insert(key, outcome.supported);
|
|
outcome
|
|
}
|
|
|
|
/// Normalize an option token to a comparable key, e.g.
|
|
/// `-XX:+UseZGC` → `UseZGC`, `G1UncommitBias=1` → `G1UncommitBias`,
|
|
/// `-XX:G1HeapRegionSize=32M` → `G1HeapRegionSize`.
|
|
fn normalize_option_key(arg: &str) -> String {
|
|
let trimmed = arg.trim_start_matches('-');
|
|
let trimmed = trimmed.strip_prefix("XX:").unwrap_or(trimmed);
|
|
let trimmed = trimmed.trim_start_matches(['+', '-']);
|
|
trimmed.split('=').next().unwrap_or(trimmed).to_string()
|
|
}
|
|
|
|
/// Scan JVM stderr for the quoted option that was rejected, and return its
|
|
/// index inside `args` when recognizable. Heuristic across HotSpot and OpenJ9
|
|
/// message formats; `None` means we can't tell, so the caller treats the whole
|
|
/// candidate as unsupported.
|
|
fn find_offending_arg(text: &str, args: &[String]) -> Option<usize> {
|
|
let quote_re = Regex::new(r"'([^']+)'").ok()?;
|
|
for captures in quote_re.captures_iter(text) {
|
|
let quoted = captures[1].trim();
|
|
if quoted.len() < 2 {
|
|
continue;
|
|
}
|
|
let key = normalize_option_key(quoted);
|
|
if key.is_empty() {
|
|
continue;
|
|
}
|
|
// Only consider quoted text that actually resembles a VM option.
|
|
let looks_like_option = quoted.contains("XX:")
|
|
|| quoted.starts_with("-XX:")
|
|
|| args.iter().any(|arg| normalize_option_key(arg) == key);
|
|
if !looks_like_option {
|
|
continue;
|
|
}
|
|
if let Some(idx) =
|
|
args.iter().position(|arg| normalize_option_key(arg) == key)
|
|
{
|
|
return Some(idx);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
const MAX_PRUNE_ROUNDS_PER_CANDIDATE: usize = 8;
|
|
const MAX_PROBES_PER_LAUNCH: usize = 14;
|
|
|
|
/// A probe that checks whether a JVM accepts a set of arguments. Abstracted so
|
|
/// the selection logic is unit-testable without spawning a real JVM.
|
|
trait JvmProbe {
|
|
fn probe<'a>(
|
|
&'a mut self,
|
|
args: &'a [String],
|
|
) -> Pin<Box<dyn Future<Output = ProbeOutcome> + Send + 'a>>;
|
|
}
|
|
|
|
/// Probe against a concrete Java binary.
|
|
struct RealJvmProbe<'a> {
|
|
java: &'a Path,
|
|
}
|
|
|
|
impl JvmProbe for RealJvmProbe<'_> {
|
|
fn probe<'a>(
|
|
&'a mut self,
|
|
args: &'a [String],
|
|
) -> Pin<Box<dyn Future<Output = ProbeOutcome> + Send + 'a>> {
|
|
Box::pin(probe_jvm_arguments(self.java, args))
|
|
}
|
|
}
|
|
|
|
/// Try each candidate in order against a probe. Pure logic so it is
|
|
/// unit-testable without spawning a real JVM.
|
|
async fn select_best_candidate_with_probe<P>(
|
|
intent: &GcLaunchIntent,
|
|
probe: &mut P,
|
|
) -> (Vec<String>, GcLaunchReport)
|
|
where
|
|
P: JvmProbe + ?Sized,
|
|
{
|
|
let mut report = GcLaunchReport {
|
|
preferred_strategy: intent.preferred_id(),
|
|
chosen_strategy: String::new(),
|
|
chosen_args: Vec::new(),
|
|
pruned_args: Vec::new(),
|
|
reason_chain: Vec::new(),
|
|
};
|
|
let mut probes = 0usize;
|
|
let candidate_count =
|
|
intent.candidates.len().min(intent.candidate_ids.len());
|
|
|
|
for index in 0..candidate_count {
|
|
let candidate_id = &intent.candidate_ids[index];
|
|
let candidate = &intent.candidates[index];
|
|
if index > 0 {
|
|
report
|
|
.reason_chain
|
|
.push(format!("{candidate_id} is the fallback candidate"));
|
|
}
|
|
|
|
let mut current = candidate.clone();
|
|
let mut pruned: Vec<String> = Vec::new();
|
|
let mut rounds = 0usize;
|
|
|
|
loop {
|
|
if probes >= MAX_PROBES_PER_LAUNCH {
|
|
report.reason_chain.push(
|
|
"JVM probe budget exhausted; using JVM default GC"
|
|
.to_string(),
|
|
);
|
|
return (Vec::new(), report);
|
|
}
|
|
probes += 1;
|
|
|
|
let outcome = probe.probe(¤t).await;
|
|
if outcome.supported {
|
|
report.chosen_strategy = candidate_id.clone();
|
|
report.chosen_args = current;
|
|
report.pruned_args = pruned;
|
|
if !report.pruned_args.is_empty() {
|
|
report.reason_chain.push(format!(
|
|
"pruned {} unsupported argument(s)",
|
|
report.pruned_args.len()
|
|
));
|
|
}
|
|
return (report.chosen_args.clone(), report);
|
|
}
|
|
|
|
if rounds >= MAX_PRUNE_ROUNDS_PER_CANDIDATE {
|
|
break;
|
|
}
|
|
rounds += 1;
|
|
|
|
match find_offending_arg(&outcome.stderr_text, ¤t) {
|
|
Some(idx) if !is_gc_selector(¤t[idx]) => {
|
|
pruned.push(current.remove(idx));
|
|
}
|
|
// Unsupported selector (or unrecognizable error): the whole
|
|
// strategy is unusable on this JVM.
|
|
_ => break,
|
|
}
|
|
}
|
|
|
|
report
|
|
.reason_chain
|
|
.push(format!("{candidate_id} is not supported by this JVM"));
|
|
}
|
|
|
|
// Every candidate failed. Launch with no GC arguments and let the JVM
|
|
// choose its default collector — never block the game over GC tuning.
|
|
report.reason_chain.push(
|
|
"no GC strategy is supported; falling back to JVM default GC"
|
|
.to_string(),
|
|
);
|
|
(Vec::new(), report)
|
|
}
|
|
|
|
/// Verify `intent.candidates` against the real JVM and select the first one
|
|
/// the JVM accepts (with surgical pruning of unsupported tuning flags).
|
|
pub async fn select_best_candidate(
|
|
java: &Path,
|
|
intent: &GcLaunchIntent,
|
|
) -> (Vec<String>, GcLaunchReport) {
|
|
select_best_candidate_with_probe(intent, &mut RealJvmProbe { java }).await
|
|
}
|
|
|
|
/// Replace the preset block (or auto marker) in `args` with the verified
|
|
/// `chosen` tokens. If the block cannot be located, leaves `args` untouched.
|
|
pub fn replace_gc_block(
|
|
args: &mut Vec<String>,
|
|
intent: &GcLaunchIntent,
|
|
chosen: &[String],
|
|
) {
|
|
let block_set: HashSet<&str> =
|
|
intent.block_tokens.iter().map(String::as_str).collect();
|
|
let is_auto = block_set.contains(AUTO_GC_PRESET_ARG)
|
|
|| intent.block_tokens.iter().any(|t| t == AUTO_GC_PRESET_ARG);
|
|
|
|
let mut out: Vec<String> = Vec::with_capacity(args.len() + chosen.len());
|
|
let mut inserted = false;
|
|
for arg in args.iter() {
|
|
let belongs_to_block = if is_auto {
|
|
arg == AUTO_GC_PRESET_ARG
|
|
} else {
|
|
block_set.contains(arg.as_str())
|
|
};
|
|
if belongs_to_block {
|
|
if !inserted {
|
|
out.extend(chosen.iter().cloned());
|
|
inserted = true;
|
|
}
|
|
} else {
|
|
out.push(arg.clone());
|
|
}
|
|
}
|
|
|
|
if !inserted {
|
|
warn!(
|
|
"GC intent block not found in effective java args; leaving them unchanged"
|
|
);
|
|
return;
|
|
}
|
|
*args = out;
|
|
}
|
|
|
|
/// Convenience: select and splice in one call, returning the report.
|
|
pub async fn resolve_gc_block(
|
|
java: &Path,
|
|
java_args: &mut Vec<String>,
|
|
intent: &crate::launcher::jvm_args::GcLaunchIntent,
|
|
) -> GcLaunchReport {
|
|
let (chosen, report) = select_best_candidate(java, intent).await;
|
|
replace_gc_block(java_args, intent, &chosen);
|
|
report
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn fake_supported(_args: &[String]) -> ProbeOutcome {
|
|
ProbeOutcome {
|
|
supported: true,
|
|
stderr_text: String::new(),
|
|
}
|
|
}
|
|
|
|
/// Wraps a synchronous fake probe so it can drive the async selection
|
|
/// logic without spawning a real JVM.
|
|
struct StubProbe<F>(F);
|
|
impl<F> JvmProbe for StubProbe<F>
|
|
where
|
|
F: FnMut(&[String]) -> ProbeOutcome,
|
|
{
|
|
fn probe<'a>(
|
|
&'a mut self,
|
|
args: &'a [String],
|
|
) -> Pin<Box<dyn Future<Output = ProbeOutcome> + Send + 'a>> {
|
|
let outcome = (self.0)(args);
|
|
Box::pin(async move { outcome })
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn normalizes_option_keys() {
|
|
assert_eq!(normalize_option_key("-XX:+UseZGC"), "UseZGC");
|
|
assert_eq!(
|
|
normalize_option_key("-XX:G1UncommitBias=1"),
|
|
"G1UncommitBias"
|
|
);
|
|
assert_eq!(normalize_option_key("UseZGC"), "UseZGC");
|
|
assert_eq!(
|
|
normalize_option_key("-XX:ShenandoahHeapRegionSize=256M"),
|
|
"ShenandoahHeapRegionSize"
|
|
);
|
|
assert_eq!(
|
|
normalize_option_key("-XX:+UnlockExperimentalVMOptions"),
|
|
"UnlockExperimentalVMOptions"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn detects_only_collector_selectors_as_gc_selectors() {
|
|
assert!(is_gc_selector("-XX:+UseZGC"));
|
|
assert!(is_gc_selector("-XX:+UseShenandoahGC"));
|
|
assert!(is_gc_selector("-XX:+UseG1GC"));
|
|
assert!(!is_gc_selector("-XX:+ZGenerational"));
|
|
assert!(!is_gc_selector("-XX:G1UncommitBias=1"));
|
|
assert!(!is_gc_selector("-XX:+UnlockExperimentalVMOptions"));
|
|
}
|
|
|
|
#[test]
|
|
fn finds_offending_tuning_flag_and_selector() {
|
|
let args = vec![
|
|
"-XX:+UseZGC".to_string(),
|
|
"-XX:+UnlockExperimentalVMOptions".to_string(),
|
|
"-XX:+ZGenerational".to_string(),
|
|
];
|
|
let text = "Unrecognized VM option 'ZGenerational'";
|
|
assert_eq!(find_offending_arg(text, &args), Some(2));
|
|
|
|
let text = "Unrecognized VM option 'UseZGC'";
|
|
assert_eq!(find_offending_arg(text, &args), Some(0));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn preferred_candidate_accepted_untouched() {
|
|
let intent = GcLaunchIntent {
|
|
active_preset_id: "gc-auto".to_string(),
|
|
block_tokens: vec!["@axolotl:gc:auto".to_string()],
|
|
candidate_ids: vec!["zgc".to_string(), "g1gc-mojang".to_string()],
|
|
candidates: vec![
|
|
vec!["-XX:+UseZGC".to_string()],
|
|
vec!["-XX:+UseG1GC".to_string()],
|
|
],
|
|
};
|
|
let (chosen, report) = select_best_candidate_with_probe(
|
|
&intent,
|
|
&mut StubProbe(fake_supported),
|
|
)
|
|
.await;
|
|
assert_eq!(report.chosen_strategy, "zgc");
|
|
assert_eq!(chosen, vec!["-XX:+UseZGC"]);
|
|
assert!(!report.fell_back());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn prunes_unsupported_tuning_flag_and_keeps_strategy() {
|
|
let intent = GcLaunchIntent {
|
|
active_preset_id: "gc-auto".to_string(),
|
|
block_tokens: vec!["@axolotl:gc:auto".to_string()],
|
|
candidate_ids: vec!["zgc".to_string()],
|
|
candidates: vec![vec![
|
|
"-XX:+UseZGC".to_string(),
|
|
"-XX:+ZGenerational".to_string(),
|
|
]],
|
|
};
|
|
// A real JVM only rejects `-XX:+ZGenerational` while it is present;
|
|
// once pruned the remaining set must be accepted.
|
|
let probe = |args: &[String]| {
|
|
if args
|
|
.iter()
|
|
.any(|a| normalize_option_key(a) == "ZGenerational")
|
|
{
|
|
ProbeOutcome {
|
|
supported: false,
|
|
stderr_text: "Unrecognized VM option 'ZGenerational'"
|
|
.to_string(),
|
|
}
|
|
} else {
|
|
fake_supported(args)
|
|
}
|
|
};
|
|
let (chosen, report) =
|
|
select_best_candidate_with_probe(&intent, &mut StubProbe(probe))
|
|
.await;
|
|
assert_eq!(report.chosen_strategy, "zgc");
|
|
assert_eq!(chosen, vec!["-XX:+UseZGC"]);
|
|
assert_eq!(report.pruned_args, vec!["-XX:+ZGenerational"]);
|
|
assert!(report.fell_back());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn selector_unsupported_falls_back_to_next_candidate() {
|
|
let intent = GcLaunchIntent {
|
|
active_preset_id: "gc-auto".to_string(),
|
|
block_tokens: vec!["@axolotl:gc:auto".to_string()],
|
|
candidate_ids: vec!["zgc".to_string(), "g1gc-mojang".to_string()],
|
|
candidates: vec![
|
|
vec!["-XX:+UseZGC".to_string()],
|
|
vec!["-XX:+UseG1GC".to_string()],
|
|
],
|
|
};
|
|
let probe = |args: &[String]| {
|
|
if args.iter().any(|a| a == "-XX:+UseZGC") {
|
|
ProbeOutcome {
|
|
supported: false,
|
|
stderr_text: "Unrecognized VM option 'UseZGC'".to_string(),
|
|
}
|
|
} else {
|
|
fake_supported(args)
|
|
}
|
|
};
|
|
let (chosen, report) =
|
|
select_best_candidate_with_probe(&intent, &mut StubProbe(probe))
|
|
.await;
|
|
assert_eq!(report.chosen_strategy, "g1gc-mojang");
|
|
assert_eq!(chosen, vec!["-XX:+UseG1GC"]);
|
|
assert!(report.fell_back());
|
|
assert!(
|
|
report
|
|
.reason_chain
|
|
.iter()
|
|
.any(|r| r.contains("not supported by this JVM"))
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn all_candidates_fail_falls_back_to_empty_block() {
|
|
let intent = GcLaunchIntent {
|
|
active_preset_id: "gc-auto".to_string(),
|
|
block_tokens: vec!["@axolotl:gc:auto".to_string()],
|
|
candidate_ids: vec![
|
|
"g1gc-mojang".to_string(),
|
|
"minimal-g1".to_string(),
|
|
],
|
|
candidates: vec![
|
|
vec!["-XX:+UseG1GC".to_string()],
|
|
vec!["-XX:+UseG1GC".to_string()],
|
|
],
|
|
};
|
|
let probe = |_args: &[String]| ProbeOutcome {
|
|
supported: false,
|
|
stderr_text: "Unrecognized VM option 'UseG1GC'".to_string(),
|
|
};
|
|
let (chosen, report) =
|
|
select_best_candidate_with_probe(&intent, &mut StubProbe(probe))
|
|
.await;
|
|
assert!(chosen.is_empty());
|
|
assert!(report.chosen_strategy.is_empty());
|
|
assert!(
|
|
report
|
|
.reason_chain
|
|
.iter()
|
|
.any(|r| r.contains("JVM default"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn replaces_auto_marker_block() {
|
|
let intent = GcLaunchIntent {
|
|
active_preset_id: "gc-auto".to_string(),
|
|
block_tokens: vec!["@axolotl:gc:auto".to_string()],
|
|
candidate_ids: vec!["g1gc-mojang".to_string()],
|
|
candidates: vec![vec!["-XX:+UseG1GC".to_string()]],
|
|
};
|
|
let mut args =
|
|
vec!["-Xmx2G".to_string(), "@axolotl:gc:auto".to_string()];
|
|
replace_gc_block(&mut args, &intent, &["-XX:+UseG1GC".to_string()]);
|
|
assert_eq!(args, vec!["-Xmx2G", "-XX:+UseG1GC"]);
|
|
}
|
|
|
|
#[test]
|
|
fn replaces_manual_preset_block_tokens() {
|
|
let intent = GcLaunchIntent {
|
|
active_preset_id: "gc-shenandoah".to_string(),
|
|
block_tokens: vec![
|
|
"-XX:+UseShenandoahGC".to_string(),
|
|
"-XX:ShenandoahHeapRegionSize=256M".to_string(),
|
|
],
|
|
candidate_ids: vec!["shenandoah".to_string()],
|
|
candidates: vec![vec![
|
|
"-XX:+UseShenandoahGC".to_string(),
|
|
"-XX:ShenandoahHeapRegionSize=256M".to_string(),
|
|
]],
|
|
};
|
|
let mut args = vec![
|
|
"-XX:+UseShenandoahGC".to_string(),
|
|
"-XX:ShenandoahHeapRegionSize=256M".to_string(),
|
|
"-Dfoo=bar".to_string(),
|
|
];
|
|
let chosen = vec!["-XX:+UseG1GC".to_string()];
|
|
replace_gc_block(&mut args, &intent, &chosen);
|
|
assert_eq!(args, vec!["-XX:+UseG1GC", "-Dfoo=bar"]);
|
|
}
|
|
|
|
#[test]
|
|
fn leaves_args_unchanged_when_block_missing() {
|
|
let intent = GcLaunchIntent {
|
|
active_preset_id: "gc-shenandoah".to_string(),
|
|
block_tokens: vec!["-XX:+UseShenandoahGC".to_string()],
|
|
candidate_ids: vec!["shenandoah".to_string()],
|
|
candidates: vec![vec!["-XX:+UseShenandoahGC".to_string()]],
|
|
};
|
|
let mut args = vec!["-Dfoo=bar".to_string()];
|
|
replace_gc_block(&mut args, &intent, &["-XX:+UseG1GC".to_string()]);
|
|
assert_eq!(args, vec!["-Dfoo=bar"]);
|
|
}
|
|
}
|