Bug 1878375 - Synchronize vendored Rust libraries with mozilla-central. r=dandarnell
mozilla-central: b7871b7f2c05e4076ad55ae4aa929b9862d4c8af comm-central: b1a0486c9d7b0ecc661142459b0aee2cf0bd321d Differential Revision: https://phabricator.services.mozilla.com/D210565 --HG-- extra : amend_source : 78a505e2b7b5028548608045f3020debd65a6445
This commit is contained in:
Родитель
ceaee73f9b
Коммит
3eb3c3c31f
|
@ -2149,9 +2149,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "glean"
|
||||
version = "60.0.0"
|
||||
version = "60.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "782325c56864d3ce57e46840b8eb9318e8d85f5b80ba88fb85bb05d47e2e119b"
|
||||
checksum = "17dbdd29dcae47b7f8fae9378ae7bf17be18b902cbb490341af2f4d04ead6bbf"
|
||||
dependencies = [
|
||||
"glean-core",
|
||||
"inherent",
|
||||
|
@ -2162,9 +2162,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "glean-core"
|
||||
version = "60.0.0"
|
||||
version = "60.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1d6ca4a985a5d5a947334338381da0b8a24cf23e8da574c1d40e74067c91b81"
|
||||
checksum = "99bf32010135ab5b1db37fff66096d08b9df4e9dd94c881aa0774df61c122fdf"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"bincode",
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"mc_workspace_toml": "76186190c57a41b30df0c6bff56ce9bbc79daa854a3205cb7569afe8f20732e95fc4119f4d20f04affd304f005e13c4936b47cdb58a5d99453c9f34884edce9e", "mc_gkrust_toml": "7b85230288493ac65c5496e68dd5b14661b51bc7667784c7afd8608bccb814195ca20d8e6a29f207190024607037f6621fa527c370be69bb9e28fc1cb51a8700", "mc_cargo_lock": "2a879d5e378177d85d57312a9b4f46136a1bfdc81098a9d48d5d7b590ece652410408b6a2479e51b8e7106dbf14700689da0a36f9d2c545d02ce38f7ca11b0bb"}
|
||||
{"mc_workspace_toml": "76186190c57a41b30df0c6bff56ce9bbc79daa854a3205cb7569afe8f20732e95fc4119f4d20f04affd304f005e13c4936b47cdb58a5d99453c9f34884edce9e", "mc_gkrust_toml": "7b85230288493ac65c5496e68dd5b14661b51bc7667784c7afd8608bccb814195ca20d8e6a29f207190024607037f6621fa527c370be69bb9e28fc1cb51a8700", "mc_cargo_lock": "72c026ee7c2b6210a9c7f7d9e9c169b29c4768e8a6b5a2980923b40c2af56a45e8cf03a6a115e4eca769fb4e34247e7874d9f68b5a6775c454f63c8af72bfafb"}
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -13,7 +13,7 @@
|
|||
edition = "2021"
|
||||
rust-version = "1.66"
|
||||
name = "glean-core"
|
||||
version = "60.0.0"
|
||||
version = "60.1.0"
|
||||
authors = [
|
||||
"Jan-Erik Rediger <jrediger@mozilla.com>",
|
||||
"The Glean Team <glean-team@mozilla.com>",
|
||||
|
|
|
@ -10,7 +10,7 @@ use std::io::BufRead;
|
|||
use std::io::BufReader;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::RwLock;
|
||||
use std::sync::{Mutex, RwLock};
|
||||
|
||||
use chrono::{DateTime, FixedOffset, Utc};
|
||||
|
||||
|
@ -96,7 +96,7 @@ pub struct EventDatabase {
|
|||
/// The in-memory list of events
|
||||
event_stores: RwLock<HashMap<String, Vec<StoredEvent>>>,
|
||||
/// A lock to be held when doing operations on the filesystem
|
||||
file_lock: RwLock<()>,
|
||||
file_lock: Mutex<()>,
|
||||
}
|
||||
|
||||
impl EventDatabase {
|
||||
|
@ -113,7 +113,7 @@ impl EventDatabase {
|
|||
Ok(Self {
|
||||
path,
|
||||
event_stores: RwLock::new(HashMap::new()),
|
||||
file_lock: RwLock::new(()),
|
||||
file_lock: Mutex::new(()),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -220,7 +220,7 @@ impl EventDatabase {
|
|||
// a lock on `event_stores`.
|
||||
// This is a potential lock-order-inversion.
|
||||
let mut db = self.event_stores.write().unwrap(); // safe unwrap, only error case is poisoning
|
||||
let _lock = self.file_lock.write().unwrap(); // safe unwrap, only error case is poisoning
|
||||
let _lock = self.file_lock.lock().unwrap(); // safe unwrap, only error case is poisoning
|
||||
|
||||
for entry in fs::read_dir(&self.path)? {
|
||||
let entry = entry?;
|
||||
|
@ -326,7 +326,7 @@ impl EventDatabase {
|
|||
/// * `store_name` - The name of the store.
|
||||
/// * `event_json` - The event content, as a single-line JSON-encoded string.
|
||||
fn write_event_to_disk(&self, store_name: &str, event_json: &str) {
|
||||
let _lock = self.file_lock.write().unwrap(); // safe unwrap, only error case is poisoning
|
||||
let _lock = self.file_lock.lock().unwrap(); // safe unwrap, only error case is poisoning
|
||||
if let Err(err) = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
|
@ -576,7 +576,7 @@ impl EventDatabase {
|
|||
.unwrap() // safe unwrap, only error case is poisoning
|
||||
.remove(&store_name.to_string());
|
||||
|
||||
let _lock = self.file_lock.write().unwrap(); // safe unwrap, only error case is poisoning
|
||||
let _lock = self.file_lock.lock().unwrap(); // safe unwrap, only error case is poisoning
|
||||
if let Err(err) = fs::remove_file(self.path.join(store_name)) {
|
||||
match err.kind() {
|
||||
std::io::ErrorKind::NotFound => {
|
||||
|
@ -596,7 +596,7 @@ impl EventDatabase {
|
|||
self.event_stores.write().unwrap().clear();
|
||||
|
||||
// safe unwrap, only error case is poisoning
|
||||
let _lock = self.file_lock.write().unwrap();
|
||||
let _lock = self.file_lock.lock().unwrap();
|
||||
std::fs::remove_dir_all(&self.path)?;
|
||||
create_dir_all(&self.path)?;
|
||||
|
||||
|
|
|
@ -195,12 +195,12 @@ mod test {
|
|||
fn accumulate_large_numbers() {
|
||||
let mut hist = Histogram::exponential(1, 500, 10);
|
||||
|
||||
hist.accumulate(u64::max_value());
|
||||
hist.accumulate(u64::max_value());
|
||||
hist.accumulate(u64::MAX);
|
||||
hist.accumulate(u64::MAX);
|
||||
|
||||
assert_eq!(2, hist.count());
|
||||
// Saturate before overflowing
|
||||
assert_eq!(u64::max_value(), hist.sum());
|
||||
assert_eq!(u64::MAX, hist.sum());
|
||||
assert_eq!(2, hist.values[&500]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,12 +167,12 @@ mod test {
|
|||
fn accumulate_large_numbers() {
|
||||
let mut hist = Histogram::linear(1, 500, 10);
|
||||
|
||||
hist.accumulate(u64::max_value());
|
||||
hist.accumulate(u64::max_value());
|
||||
hist.accumulate(u64::MAX);
|
||||
hist.accumulate(u64::MAX);
|
||||
|
||||
assert_eq!(2, hist.count());
|
||||
// Saturate before overflowing
|
||||
assert_eq!(u64::max_value(), hist.sum());
|
||||
assert_eq!(u64::MAX, hist.sum());
|
||||
assert_eq!(2, hist.values[&500]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::common_metric_data::CommonMetricDataInternal;
|
||||
use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
|
||||
|
@ -412,6 +413,35 @@ impl TimingDistributionMetric {
|
|||
})
|
||||
}
|
||||
|
||||
/// Accumulates precisely one duration to the metric.
|
||||
///
|
||||
/// Like `TimingDistribution::accumulate_single_sample`, but for use when the
|
||||
/// duration is:
|
||||
///
|
||||
/// * measured externally, or
|
||||
/// * is in a unit different from the timing_distribution's internal TimeUnit.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `duration` - The single duration to be recorded in the metric.
|
||||
///
|
||||
/// ## Notes
|
||||
///
|
||||
/// Reports an [`ErrorType::InvalidOverflow`] error if `duration` is longer than
|
||||
/// `MAX_SAMPLE_TIME`.
|
||||
///
|
||||
/// The API client is responsible for ensuring that `duration` is derived from a
|
||||
/// monotonic clock source that behaves consistently over computer sleep across
|
||||
/// the application's platforms. Otherwise the resulting data may not share the same
|
||||
/// guarantees that other `timing_distribution` metrics' data do.
|
||||
pub fn accumulate_raw_duration(&self, duration: Duration) {
|
||||
let duration_ns = duration.as_nanos().try_into().unwrap_or(u64::MAX);
|
||||
let metric = self.clone();
|
||||
crate::launch_with_glean(move |glean| {
|
||||
metric.accumulate_raw_samples_nanos_sync(glean, &[duration_ns])
|
||||
})
|
||||
}
|
||||
|
||||
/// **Test-only API (exported for testing purposes).**
|
||||
///
|
||||
/// Accumulates the provided samples in the metric.
|
||||
|
|
|
@ -6,6 +6,8 @@ use crate::metrics::DistributionData;
|
|||
use crate::metrics::TimerId;
|
||||
use crate::ErrorType;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
/// A description for the [`TimingDistributionMetric`](crate::metrics::TimingDistributionMetric) type.
|
||||
///
|
||||
/// When changing this trait, make sure all the operations are
|
||||
|
@ -103,6 +105,29 @@ pub trait TimingDistribution {
|
|||
/// are longer than `MAX_SAMPLE_TIME`.
|
||||
fn accumulate_raw_samples_nanos(&self, samples: Vec<u64>);
|
||||
|
||||
/// Accumulates precisely one duration to the metric.
|
||||
///
|
||||
/// Like `TimingDistribution::accumulate_single_sample`, but for use when the
|
||||
/// duration is:
|
||||
///
|
||||
/// * measured externally, or
|
||||
/// * is in a unit different from the timing_distribution's internal TimeUnit.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `duration` - The single duration to be recorded in the metric.
|
||||
///
|
||||
/// ## Notes
|
||||
///
|
||||
/// Reports an [`ErrorType::InvalidOverflow`] error if `duration` is longer than
|
||||
/// `MAX_SAMPLE_TIME`.
|
||||
///
|
||||
/// The API client is responsible for ensuring that `duration` is derived from a
|
||||
/// monotonic clock source that behaves consistently over computer sleep across
|
||||
/// the application's platforms. Otherwise the resulting data may not share the same
|
||||
/// guarantees that other `timing_distribution` metrics' data do.
|
||||
fn accumulate_raw_duration(&self, duration: Duration);
|
||||
|
||||
/// **Exported for test purposes.**
|
||||
///
|
||||
/// Gets the currently stored value of the metric.
|
||||
|
|
|
@ -168,10 +168,7 @@ fn saturates_at_boundary() {
|
|||
});
|
||||
|
||||
counter.add_sync(&glean, 2);
|
||||
counter.add_sync(&glean, i32::max_value());
|
||||
counter.add_sync(&glean, i32::MAX);
|
||||
|
||||
assert_eq!(
|
||||
i32::max_value(),
|
||||
counter.get_value(&glean, Some("store1")).unwrap()
|
||||
);
|
||||
assert_eq!(i32::MAX, counter.get_value(&glean, Some("store1")).unwrap());
|
||||
}
|
||||
|
|
|
@ -296,7 +296,7 @@ fn large_nanoseconds_values() {
|
|||
);
|
||||
|
||||
let time = Duration::from_secs(10).as_nanos() as u64;
|
||||
assert!(time > u64::from(u32::max_value()));
|
||||
assert!(time > u64::from(u32::MAX));
|
||||
|
||||
let id = 4u64.into();
|
||||
metric.set_start(id, 0);
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"files":{"Cargo.toml":"e13e9991a07e8b9b60f2be6642b3c3b046d1a93665eb5862190412cac1f2fea4","LICENSE":"1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5","README.md":"5627cc81e6187ab6c2b4dff061af16d559edcab64ba786bac39daa69c703c595","src/common_test.rs":"de47b53dcca37985c0a2b8c02daecbf32309aa54f5a4dd9290719c2c1fd0fa55","src/configuration.rs":"27075b12236021c54d0c99427bcbd417933ca02545275604d3c13f32ca25af13","src/core_metrics.rs":"fef8fb4e5fa57c179836c6eb2cf59278fe3b8b036dbe57b0ff02971b4acd822f","src/lib.rs":"3a43c992275f06c4189ffc9ca0775fe840b5598c2f6a39504aea3ae5df43fa23","src/net/http_uploader.rs":"01ad5bd91384411a12c74434cd1c5cd585078cb34faba4615c70bdb669a9bccb","src/net/mod.rs":"f47b96bb878f1a6c771cedbaeaeefb270bc87fb1d1bbbed1b282dddca16216ed","src/private/event.rs":"d7c70c02648584c19c73af89e5180d3c6153c911f2c6830f7d1599b18d6150eb","src/private/mod.rs":"3565eb569d2b96f938f130abe0fc3ee3f55e7e03fd6501e309d3ef6af72ef6ee","src/private/object.rs":"3f70363a196aea46cc163af025a53e48c117c6208babc4bce772bb4c337cced8","src/private/ping.rs":"31d33d7f661a7a17ccb69351328700b4d7b80024d1e128f406c3534f9d163475","src/system.rs":"6eae5b41c15eba9cad6dbd116abe3519ee3e1fe034e79bdd692b029829a8c384","src/test.rs":"39dd7468dcdaf17593b8b07970ced25c07cbd76853aaef2532fdcad0281a21d3","tests/common/mod.rs":"08fb9483d9b6ed9fe873b4395245166ae8a15263be750c7a8e298c41d9604745","tests/init_fails.rs":"46d7064bba9386c3065635434e17ac9212c6c2236b3cd12bd985fc3229e659a3","tests/never_init.rs":"7a6e8a011fbd945f2544f204367eeceff3f6039c99d98799477e3b2352ae6227","tests/no_time_to_init.rs":"4a5bdddc2f8226d2ad17038229e8767a6dd195977af49527fbb84a9f6b0154bb","tests/overflowing_preinit.rs":"7ad4b2274dd9240b53430859a4eb1d2597cf508a5a678333f3d3abbadd2ed4a7","tests/persist_ping_lifetime.rs":"81415dc1d74743f02269f0d0dfa524003147056853f080276972e64a0b761d3c","tests/persist_ping_lifetime_nopanic.rs":"18379d3ffbf4a2c8c684c04ff7a0660b86dfbbb447db2d24dfed6073cb7ddf8f","tests/schema.rs":"dde65bce8a715ca3bd9c54b2466d831dd5e0d559e0773fe7657827f22a66bb44","tests/simple.rs":"1835b5df6f76ff894b45805bd54eaab23ca2d9d2b0694ec64af3aa6132baf30e","tests/test-shutdown-blocking.sh":"a44d8d4bbe2ee3ede9e48121150ae7a5386025160c5cef2181ca142232c5fb27","tests/test-thread-crashing.sh":"8d5ed070754e09fbe55183bb2792ae6e234a95770e39397caf05e4ec4d6015db","tests/upload_timing.rs":"6a97aa355d808123af0914ffecf1da0ecb2cc441c95c63c600b14f97ce0d45a0"},"package":"782325c56864d3ce57e46840b8eb9318e8d85f5b80ba88fb85bb05d47e2e119b"}
|
||||
{"files":{"Cargo.toml":"78a1972b9bbfcd28816592f60f1a504adcfd041936ce5d0fec3e0788391c784a","LICENSE":"1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5","README.md":"5627cc81e6187ab6c2b4dff061af16d559edcab64ba786bac39daa69c703c595","src/common_test.rs":"de47b53dcca37985c0a2b8c02daecbf32309aa54f5a4dd9290719c2c1fd0fa55","src/configuration.rs":"27075b12236021c54d0c99427bcbd417933ca02545275604d3c13f32ca25af13","src/core_metrics.rs":"fef8fb4e5fa57c179836c6eb2cf59278fe3b8b036dbe57b0ff02971b4acd822f","src/lib.rs":"3a43c992275f06c4189ffc9ca0775fe840b5598c2f6a39504aea3ae5df43fa23","src/net/http_uploader.rs":"01ad5bd91384411a12c74434cd1c5cd585078cb34faba4615c70bdb669a9bccb","src/net/mod.rs":"f47b96bb878f1a6c771cedbaeaeefb270bc87fb1d1bbbed1b282dddca16216ed","src/private/event.rs":"d7c70c02648584c19c73af89e5180d3c6153c911f2c6830f7d1599b18d6150eb","src/private/mod.rs":"3565eb569d2b96f938f130abe0fc3ee3f55e7e03fd6501e309d3ef6af72ef6ee","src/private/object.rs":"3f70363a196aea46cc163af025a53e48c117c6208babc4bce772bb4c337cced8","src/private/ping.rs":"31d33d7f661a7a17ccb69351328700b4d7b80024d1e128f406c3534f9d163475","src/system.rs":"6eae5b41c15eba9cad6dbd116abe3519ee3e1fe034e79bdd692b029829a8c384","src/test.rs":"39dd7468dcdaf17593b8b07970ced25c07cbd76853aaef2532fdcad0281a21d3","tests/common/mod.rs":"08fb9483d9b6ed9fe873b4395245166ae8a15263be750c7a8e298c41d9604745","tests/init_fails.rs":"46d7064bba9386c3065635434e17ac9212c6c2236b3cd12bd985fc3229e659a3","tests/never_init.rs":"7a6e8a011fbd945f2544f204367eeceff3f6039c99d98799477e3b2352ae6227","tests/no_time_to_init.rs":"4a5bdddc2f8226d2ad17038229e8767a6dd195977af49527fbb84a9f6b0154bb","tests/overflowing_preinit.rs":"7ad4b2274dd9240b53430859a4eb1d2597cf508a5a678333f3d3abbadd2ed4a7","tests/persist_ping_lifetime.rs":"81415dc1d74743f02269f0d0dfa524003147056853f080276972e64a0b761d3c","tests/persist_ping_lifetime_nopanic.rs":"18379d3ffbf4a2c8c684c04ff7a0660b86dfbbb447db2d24dfed6073cb7ddf8f","tests/schema.rs":"dde65bce8a715ca3bd9c54b2466d831dd5e0d559e0773fe7657827f22a66bb44","tests/simple.rs":"1835b5df6f76ff894b45805bd54eaab23ca2d9d2b0694ec64af3aa6132baf30e","tests/test-shutdown-blocking.sh":"a44d8d4bbe2ee3ede9e48121150ae7a5386025160c5cef2181ca142232c5fb27","tests/test-thread-crashing.sh":"8d5ed070754e09fbe55183bb2792ae6e234a95770e39397caf05e4ec4d6015db","tests/timing_distribution_single_sample.rs":"fddf2f13f1620a8808029d250a64e4c822828bf80b4bb4f9e3b645ab70643f9b","tests/upload_timing.rs":"6a97aa355d808123af0914ffecf1da0ecb2cc441c95c63c600b14f97ce0d45a0"},"package":"17dbdd29dcae47b7f8fae9378ae7bf17be18b902cbb490341af2f4d04ead6bbf"}
|
|
@ -13,7 +13,7 @@
|
|||
edition = "2021"
|
||||
rust-version = "1.66"
|
||||
name = "glean"
|
||||
version = "60.0.0"
|
||||
version = "60.1.0"
|
||||
authors = [
|
||||
"Jan-Erik Rediger <jrediger@mozilla.com>",
|
||||
"The Glean Team <glean-team@mozilla.com>",
|
||||
|
@ -35,7 +35,7 @@ license = "MPL-2.0"
|
|||
repository = "https://github.com/mozilla/glean"
|
||||
|
||||
[dependencies.glean-core]
|
||||
version = "60.0.0"
|
||||
version = "60.1.0"
|
||||
|
||||
[dependencies.inherent]
|
||||
version = "1"
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
//! This integration test should model how the RLB is used when embedded in another Rust application
|
||||
//! (e.g. FOG/Firefox Desktop).
|
||||
//!
|
||||
//! We write a single test scenario per file to avoid any state keeping across runs
|
||||
//! (different files run as different processes).
|
||||
|
||||
mod common;
|
||||
|
||||
use glean::{ConfigurationBuilder, ErrorType};
|
||||
use std::time::Duration;
|
||||
|
||||
/// A timing_distribution
|
||||
mod metrics {
|
||||
use glean::private::*;
|
||||
use glean::{Lifetime, TimeUnit};
|
||||
use glean_core::CommonMetricData;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub static boo: Lazy<TimingDistributionMetric> = Lazy::new(|| {
|
||||
TimingDistributionMetric::new(
|
||||
CommonMetricData {
|
||||
name: "boo".into(),
|
||||
category: "sample".into(),
|
||||
send_in_pings: vec!["validation".into()],
|
||||
lifetime: Lifetime::Ping,
|
||||
disabled: false,
|
||||
..Default::default()
|
||||
},
|
||||
TimeUnit::Millisecond,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
/// Test scenario: Ensure single-sample accumulation works.
|
||||
#[test]
|
||||
fn raw_duration_works() {
|
||||
common::enable_test_logging();
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let tmpname = dir.path().to_path_buf();
|
||||
|
||||
let cfg = ConfigurationBuilder::new(true, tmpname, "firefox-desktop")
|
||||
.with_server_endpoint("invalid-test-host")
|
||||
.build();
|
||||
common::initialize(cfg);
|
||||
|
||||
metrics::boo.accumulate_raw_duration(Duration::from_secs(1));
|
||||
|
||||
assert_eq!(
|
||||
1,
|
||||
metrics::boo.test_get_value(None).unwrap().count,
|
||||
"Single sample: single count"
|
||||
);
|
||||
|
||||
// Let's check overflow.
|
||||
metrics::boo.accumulate_raw_duration(Duration::from_nanos(u64::MAX));
|
||||
|
||||
assert_eq!(2, metrics::boo.test_get_value(None).unwrap().count);
|
||||
assert_eq!(
|
||||
1,
|
||||
metrics::boo.test_get_num_recorded_errors(ErrorType::InvalidOverflow)
|
||||
);
|
||||
|
||||
glean::shutdown(); // Cleanly shut down at the end of the test.
|
||||
}
|
Загрузка…
Ссылка в новой задаче