Skip to main content

opendal_testkit/
utils.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::collections::HashMap;
19use std::env;
20use std::sync::LazyLock;
21
22use opendal_core::Operator;
23use opendal_core::Result;
24use opendal_core::layers::CapabilityOverrideLayer;
25use opendal_layer_logging::LoggingLayer;
26use opendal_layer_retry::RetryLayer;
27use opendal_layer_timeout::TimeoutLayer;
28use sha2::Digest;
29use sha2::Sha256;
30
31const OPENDAL_TEST_CAPABILITY_OVERRIDES: &str = "OPENDAL_TEST_CAPABILITY_OVERRIDES";
32const OPENDAL_TEST_UNSET_VALUE: &str = "__OPENDAL_TEST_UNSET__";
33
34pub(crate) fn sha256_digest(data: impl AsRef<[u8]>) -> String {
35    use std::fmt::Write;
36
37    let digest = Sha256::digest(data);
38    let mut output = String::with_capacity(digest.len() * 2);
39    for byte in digest {
40        write!(&mut output, "{byte:02x}").expect("writing to String must succeed");
41    }
42    output
43}
44
45/// TEST_RUNTIME is the runtime used for running tests.
46pub static TEST_RUNTIME: LazyLock<tokio::runtime::Runtime> = LazyLock::new(|| {
47    tokio::runtime::Builder::new_multi_thread()
48        .enable_all()
49        .build()
50        .unwrap()
51});
52
53fn collect_config(
54    prefix: &str,
55    vars: impl IntoIterator<Item = (String, String)>,
56) -> HashMap<String, String> {
57    vars.into_iter()
58        .filter_map(|(k, v)| {
59            if v == OPENDAL_TEST_UNSET_VALUE {
60                return None;
61            }
62            k.to_lowercase()
63                .strip_prefix(prefix)
64                .map(|k| (k.to_string(), v))
65        })
66        .collect()
67}
68
69/// Init a service with given scheme.
70///
71/// - Load scheme from `OPENDAL_TEST`
72/// - Construct a new Operator with given root.
73/// - Else, returns a `None` to represent no valid config for operator.
74pub fn init_test_service() -> Result<Option<Operator>> {
75    let _ = dotenvy::dotenv();
76
77    let scheme = if let Ok(v) = env::var("OPENDAL_TEST") {
78        v
79    } else {
80        return Ok(None);
81    };
82
83    let prefix = {
84        let scheme_key = scheme.replace('-', "_");
85        format!("opendal_{scheme_key}_")
86    };
87
88    let mut cfg = collect_config(&prefix, env::vars());
89
90    // Use random root unless OPENDAL_DISABLE_RANDOM_ROOT is set to true.
91    let disable_random_root = env::var("OPENDAL_DISABLE_RANDOM_ROOT").unwrap_or_default() == "true";
92    if !disable_random_root {
93        let root = format!(
94            "{}{}/",
95            cfg.get("root").cloned().unwrap_or_else(|| "/".to_string()),
96            uuid::Uuid::new_v4()
97        );
98        cfg.insert("root".to_string(), root);
99    }
100
101    // string-based scheme uses a hyphen ('-') as the connector
102    let scheme = scheme.replace('_', "-");
103    let mut op = Operator::via_iter(scheme, cfg).expect("must succeed");
104
105    if let Ok(overrides) = env::var(OPENDAL_TEST_CAPABILITY_OVERRIDES)
106        && overrides != OPENDAL_TEST_UNSET_VALUE
107    {
108        op = op.layer(CapabilityOverrideLayer::from_overrides(&overrides)?);
109    }
110
111    let op = op
112        .layer(LoggingLayer::default())
113        .layer(TimeoutLayer::new())
114        .layer(RetryLayer::new().with_max_times(4));
115
116    Ok(Some(op))
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_collect_config_skips_unset_values() {
125        let cfg = collect_config(
126            "opendal_s3_",
127            [
128                (
129                    "OPENDAL_S3_ENDPOINT".to_string(),
130                    "http://localhost".to_string(),
131                ),
132                (
133                    "OPENDAL_S3_ALLOW_ANONYMOUS".to_string(),
134                    OPENDAL_TEST_UNSET_VALUE.to_string(),
135                ),
136                ("OPENDAL_S3_PASSWORD".to_string(), String::new()),
137                ("OPENDAL_GCS_BUCKET".to_string(), "test".to_string()),
138            ],
139        );
140
141        assert_eq!(
142            cfg.get("endpoint").map(String::as_str),
143            Some("http://localhost")
144        );
145        assert_eq!(cfg.get("password").map(String::as_str), Some(""));
146        assert!(!cfg.contains_key("allow_anonymous"));
147        assert!(!cfg.contains_key("bucket"));
148    }
149}