Merge branch 'master' into optimize

This commit is contained in:
vms 2021-09-03 20:12:03 +03:00
commit b13d558ab5
15 changed files with 220 additions and 452 deletions

44
.circleci/config.yml Normal file
View File

@ -0,0 +1,44 @@
version: 2.1
orbs:
docker: circleci/docker@1.5.0
jobs:
sqlite_connector_rust_tests:
docker:
- image: circleci/rust:latest
resource_class: xlarge
environment:
RUST_BACKTRACE: full
steps:
- checkout
- restore_cache:
keys:
- sqlite-connector01-{{ checksum "Cargo.lock" }}
- run: |
rustup toolchain install nightly-2021-05-21
rustup default nightly-2021-05-21
rustup override set nightly-2021-05-21
rustup component add rustfmt --toolchain nightly-2021-05-21
rustup component add clippy --toolchain nightly-2021-05-21
rustup target add wasm32-wasi
cargo install marine
cargo fmt --all -- --check --color always
cargo check -v --all-features
./build.sh
cargo test --release -v --all-features
- save_cache:
paths:
- ~/.cargo
- ~/.rustup
key: sqlite-connector01-{{ checksum "Cargo.lock" }}
workflows:
version: 2.1
marine:
jobs:
- sqlite_connector_rust_tests

14
.github/download_marine.sh vendored Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
set -o pipefail -o errexit -o nounset
set -x
MARINE_RELEASE="https://api.github.com/repos/fluencelabs/marine/releases/latest"
OUT_DIR=/usr/local/bin
# get metadata about release
curl -s -H "Accept: application/vnd.github.v3+json" $MARINE_RELEASE |
# extract url and name for asset with name "marine"
# also append $OUT_DIR to each name so file is saved to $OUT_DIR
jq -r ".assets | .[] | select(.name == \"marine\") | \"\(.browser_download_url) $OUT_DIR/\(.name)\"" |
# download assets
xargs -n2 bash -c 'curl -L $0 -o $1 && chmod +x $1'

View File

@ -1,23 +0,0 @@
language: rust
os:
- linux
- osx
rust:
- stable
- beta
- nightly
script:
- if [ "$TRAVIS_RUST_VERSION" = "nightly" ]; then
cargo bench;
cargo build;
cargo test;
else
cargo build;
cargo test;
fi
notifications:
email: false

View File

@ -21,6 +21,7 @@ repository = "https://github.com/stainless-steel/sqlite"
readme = "README.md"
categories = ["api-bindings", "database"]
keywords = ["database"]
edition = "2018"
[lib]
name = "marine_sqlite_connector"
@ -39,3 +40,4 @@ marine-rs-sdk = "0.6.10"
[dev-dependencies]
temporary = "0.6"
marine-rs-sdk-test = "0.2.0"

19
Config.toml Normal file
View File

@ -0,0 +1,19 @@
modules_dir = "artifacts/"
[[module]]
name = "sqlite3"
mem_pages_count = 100
logger_enabled = false
[module.wasi]
preopened_files = ["/tmp"]
mapped_dirs = { "tmp" = "/tmp" }
[[module]]
name = "test"
mem_pages_count = 1
logger_enabled = false
[module.wasi]
preopened_files = ["/tmp"]
mapped_dirs = { "tmp" = "/tmp" }

16
build.sh Executable file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -o errexit -o nounset -o pipefail
# set current working directory to script directory to run script from everywhere
cd "$(dirname "$0")"
# build test.wasm
marine build --release --bin test
# copy .wasm to artifacts
rm -f artifacts/*
mkdir -p artifacts
cp target/wasm32-wasi/release/test.wasm artifacts/
# download SQLite 3 to use in tests
curl -L https://github.com/fluencelabs/sqlite/releases/download/v0.15.0_w/sqlite3.wasm -o artifacts/sqlite3.wasm

View File

@ -1,10 +1,9 @@
use sqlite3_connector as ffi;
use crate::sqlite3_connector as ffi;
use crate::{Result, Statement};
use std::marker::PhantomData;
use std::path::Path;
use {Result, Statement};
/// A database connection.
pub struct Connection {
raw: ffi::Sqlite3DbHandle,
@ -33,14 +32,14 @@ impl Connection {
match result.ret_code {
ffi::SQLITE_OK => {}
code => {
return match ::last_error(result.db_handle) {
return match crate::last_error(result.db_handle) {
Some(error) => {
ffi::sqlite3_close(result.db_handle);
Err(error)
}
_ => {
ffi::sqlite3_close(result.db_handle);
Err(::Error {
Err(crate::Error {
code: Some(code as isize),
message: None,
})
@ -91,7 +90,7 @@ impl Connection {
/// Create a prepared statement.
#[inline]
pub fn prepare<T: AsRef<str>>(&self, statement: T) -> Result<Statement> {
::statement::new(self.raw, statement)
crate::statement::new(self.raw, statement)
}
/// Return the number of rows inserted, updated, or deleted by the most

View File

@ -1,6 +1,6 @@
use sqlite3_connector as ffi;
use statement::{State, Statement};
use {Result, Value};
use crate::sqlite3_connector as ffi;
use crate::statement::{State, Statement};
use crate::{Result, Value};
/// An iterator over rows.
pub struct Cursor {

View File

@ -1,8 +1,6 @@
#![allow(unused_variables)]
#![allow(non_snake_case)]
extern crate marine_rs_sdk;
use marine_rs_sdk::marine;
pub fn main() {}

View File

@ -4,7 +4,7 @@
//!
//! Open a connection, create a table, and insert some rows:
//!
//! ```
//! ```ignore
//! let connection = sqlite::open(":memory:").unwrap();
//!
//! connection
@ -20,7 +20,7 @@
//!
//! Select some rows and process them one by one as plain text:
//!
//! ```
//! ```ignore
//! # let connection = sqlite::open(":memory:").unwrap();
//! # connection
//! # .execute(
@ -44,7 +44,7 @@
//! The same query using a prepared statement, which is much more efficient than
//! the previous technique:
//!
//! ```
//! ```ignore
//! use sqlite::State;
//! # let connection = sqlite::open(":memory:").unwrap();
//! # connection
@ -72,7 +72,7 @@
//! The same query using a cursor, which is a wrapper around a prepared
//! statement providing the concept of row and featuring all-at-once binding:
//!
//! ```
//! ```ignore
//! use sqlite::Value;
//! # let connection = sqlite::open(":memory:").unwrap();
//! # connection
@ -108,9 +108,9 @@ use std::{error, fmt};
macro_rules! error(
($connection:expr, $code:expr) => (
match ::last_error($connection) {
match crate::last_error($connection) {
Some(error) => return Err(error),
_ => return Err(::Error {
_ => return Err(crate::Error {
code: Some($code as isize),
message: None,
}),
@ -121,7 +121,7 @@ macro_rules! error(
macro_rules! ok_descr(
($connection:expr, $result:expr) => (
match $result.ret_code {
::ffi::SQLITE_OK => {}
crate::ffi::SQLITE_OK => {}
code => error!($connection, code),
}
);
@ -139,7 +139,7 @@ macro_rules! ok_descr(
macro_rules! ok_raw(
($connection:expr, $result:expr) => (
match $result {
::ffi::SQLITE_OK => {}
crate::ffi::SQLITE_OK => {}
code => error!($connection, code),
}
);

View File

@ -1,6 +1,4 @@
extern crate marine_rs_sdk;
use self::marine_rs_sdk::marine;
use marine_rs_sdk::marine;
pub(crate) type Sqlite3DbHandle = u32;
pub(crate) type Sqlite3StmtHandle = u32;

View File

@ -1,7 +1,7 @@
use sqlite3_connector as ffi;
use std::marker::PhantomData;
use crate::sqlite3_connector as ffi;
use crate::{Cursor, Result, Type, Value};
use {Cursor, Result, Type, Value};
use std::marker::PhantomData;
/// A prepared statement.
pub struct Statement {
@ -31,7 +31,7 @@ pub trait Readable: Sized {
/// Read from a column.
///
/// The leftmost column has the index 0.
fn read(&Statement, usize) -> Result<Self>;
fn read(_: &Statement, _: usize) -> Result<Self>;
}
impl Statement {
@ -108,7 +108,7 @@ impl Statement {
/// Upgrade to a cursor.
#[inline]
pub fn cursor(self) -> Cursor {
::cursor::new(self)
crate::cursor::new(self)
}
/// Return the raw pointer.

View File

@ -1,8 +1,6 @@
extern crate marine_rs_sdk;
extern crate marine_sqlite_connector;
use marine_rs_sdk::marine;
use marine_sqlite_connector::State;
use marine_sqlite_connector::Value;
pub fn main() {}
@ -10,31 +8,6 @@ pub fn main() {}
pub fn test1() {
let connection = marine_sqlite_connector::open(":memory:").unwrap();
connection
.execute(
"
CREATE TABLE users (name TEXT, age INTEGER);
INSERT INTO users VALUES ('Alice', 42);
INSERT INTO users VALUES ('Bob', 69);
",
)
.unwrap();
connection
.iterate("SELECT * FROM users WHERE age > 50", |pairs| {
for &(column, value) in pairs.iter() {
println!("{} = {}", column, value.unwrap());
}
true
})
.unwrap();
}
#[marine]
pub fn test2() {
let connection = marine_sqlite_connector::open(":memory:").unwrap();
println!("connection id = {}\n", connection.as_raw());
connection
.execute(
"
@ -51,15 +24,13 @@ pub fn test2() {
statement.bind(1, 50).unwrap();
while let State::Row = statement.next().unwrap() {
println!("name = {}", statement.read::<String>(0).unwrap());
println!("age = {}", statement.read::<i64>(1).unwrap());
}
assert_eq!(statement.next().unwrap(), State::Row);
assert_eq!(statement.read::<String>(0).unwrap(), "Bob");
assert_eq!(statement.read::<i64>(1).unwrap(), 69);
}
#[marine]
pub fn test3() {
use marine_sqlite_connector::Value;
#[marine]
pub fn test2() {
let connection = marine_sqlite_connector::open(":memory:").unwrap();
connection
@ -80,7 +51,63 @@ pub fn test3() {
cursor.bind(&[Value::Integer(50)]).unwrap();
while let Some(row) = cursor.next().unwrap() {
println!("name = {}", row[0].as_string().unwrap());
println!("age = {}", row[1].as_integer().unwrap());
assert_eq!(row[0].as_string().unwrap(), "Bob");
assert_eq!(row[1].as_integer().unwrap(), 69);
}
}
#[marine]
pub fn test3() {
let connection = marine_sqlite_connector::open(":memory:").unwrap();
connection
.execute(
"
CREATE TABLE test (number INTEGER, blob BLOB NOT NULL);
",
)
.unwrap();
let mut cursor = connection
.prepare("INSERT OR REPLACE INTO test VALUES (?, ?)")
.unwrap();
cursor.bind(1, &Value::Integer(50)).unwrap();
cursor.bind(2, &Value::Binary(vec![1, 2, 3])).unwrap();
// check that blob is not null
assert!(cursor.next().is_ok());
}
#[marine]
pub fn test4() {
let connection = marine_sqlite_connector::open(":memory:").unwrap();
connection
.execute(
"
CREATE TABLE test (number INTEGER, blob BLOB);
",
)
.unwrap();
let mut cursor = connection
.prepare("INSERT OR REPLACE INTO test VALUES (?, ?)")
.unwrap();
cursor.bind(1, &Value::Integer(50)).unwrap();
cursor.bind(2, &Value::Binary(vec![1, 2, 3])).unwrap();
cursor.next().unwrap();
let mut cursor = connection
.prepare("SELECT blob FROM test WHERE number = ?")
.unwrap()
.cursor();
cursor.bind(&[Value::Integer(50)]).unwrap();
while let Some(row) = cursor.next().unwrap() {
assert_eq!(row[0].as_binary().unwrap().to_vec(), vec![1, 2, 3]);
}
}

View File

@ -1,365 +0,0 @@
extern crate sqlite;
extern crate temporary;
use sqlite::{Connection, OpenFlags, State, Type, Value};
use std::path::Path;
macro_rules! ok(($result:expr) => ($result.unwrap()));
#[test]
fn connection_changes() {
let connection = setup_users(":memory:");
assert_eq!(connection.changes(), 1);
assert_eq!(connection.total_changes(), 1);
ok!(connection.execute("INSERT INTO users VALUES (2, 'Bob', NULL, NULL, NULL)"));
assert_eq!(connection.changes(), 1);
assert_eq!(connection.total_changes(), 2);
ok!(connection.execute("UPDATE users SET name = 'Bob' WHERE id = 1"));
assert_eq!(connection.changes(), 1);
assert_eq!(connection.total_changes(), 3);
ok!(connection.execute("DELETE FROM users"));
assert_eq!(connection.changes(), 2);
assert_eq!(connection.total_changes(), 5);
}
#[test]
fn connection_error() {
let connection = setup_users(":memory:");
match connection.execute(":)") {
Err(error) => assert_eq!(
error.message,
Some(String::from(r#"unrecognized token: ":""#))
),
_ => unreachable!(),
}
}
#[test]
fn connection_iterate() {
macro_rules! pair(
($one:expr, $two:expr) => (($one, Some($two)));
);
let connection = setup_users(":memory:");
let mut done = false;
let statement = "SELECT * FROM users";
ok!(connection.iterate(statement, |pairs| {
assert_eq!(pairs.len(), 5);
assert_eq!(pairs[0], pair!("id", "1"));
assert_eq!(pairs[1], pair!("name", "Alice"));
assert_eq!(pairs[2], pair!("age", "42.69"));
assert_eq!(pairs[3], pair!("photo", "\x42\x69"));
assert_eq!(pairs[4], ("email", None));
done = true;
true
}));
assert!(done);
}
#[test]
fn connection_open_with_flags() {
use temporary::Directory;
let directory = ok!(Directory::new("sqlite"));
let path = directory.path().join("database.sqlite3");
setup_users(&path);
let flags = OpenFlags::new().set_read_only();
let connection = ok!(Connection::open_with_flags(path, flags));
match connection.execute("INSERT INTO users VALUES (2, 'Bob', NULL, NULL)") {
Err(_) => {}
_ => unreachable!(),
}
}
#[test]
fn connection_set_busy_handler() {
use std::thread;
use temporary::Directory;
let directory = ok!(Directory::new("sqlite"));
let path = directory.path().join("database.sqlite3");
setup_users(&path);
let guards = (0..100)
.map(|_| {
let path = path.to_path_buf();
thread::spawn(move || {
let mut connection = ok!(sqlite::open(&path));
ok!(connection.set_busy_handler(|_| true));
let statement = "INSERT INTO users VALUES (?, ?, ?, ?, ?)";
let mut statement = ok!(connection.prepare(statement));
ok!(statement.bind(1, 2i64));
ok!(statement.bind(2, "Bob"));
ok!(statement.bind(3, 69.42));
ok!(statement.bind(4, &[0x69u8, 0x42u8][..]));
ok!(statement.bind(5, ()));
assert_eq!(ok!(statement.next()), State::Done);
true
})
})
.collect::<Vec<_>>();
for guard in guards {
assert!(ok!(guard.join()));
}
}
#[test]
fn cursor_read() {
let connection = setup_users(":memory:");
ok!(connection.execute("INSERT INTO users VALUES (2, 'Bob', NULL, NULL, NULL)"));
let statement = "SELECT id, age FROM users ORDER BY 1 DESC";
let statement = ok!(connection.prepare(statement));
let mut count = 0;
let mut cursor = statement.cursor();
while let Some(row) = ok!(cursor.next()) {
let id = row[0].as_integer().unwrap();
if id == 1 {
assert_eq!(row[1].as_float().unwrap(), 42.69);
} else if id == 2 {
assert_eq!(row[1].as_float().unwrap_or(69.42), 69.42);
} else {
assert!(false);
}
count += 1;
}
assert_eq!(count, 2);
}
#[test]
fn cursor_wildcard() {
let connection = setup_english(":memory:");
let statement = "SELECT value FROM english WHERE value LIKE '%type'";
let statement = ok!(connection.prepare(statement));
let mut count = 0;
let mut cursor = statement.cursor();
while let Some(_) = ok!(cursor.next()) {
count += 1;
}
assert_eq!(count, 6);
}
#[test]
fn cursor_wildcard_with_binding() {
let connection = setup_english(":memory:");
let statement = "SELECT value FROM english WHERE value LIKE ?";
let mut statement = ok!(connection.prepare(statement));
ok!(statement.bind(1, "%type"));
let mut count = 0;
let mut cursor = statement.cursor();
while let Some(_) = ok!(cursor.next()) {
count += 1;
}
assert_eq!(count, 6);
}
#[test]
fn cursor_workflow() {
let connection = setup_users(":memory:");
let select = "SELECT id, name FROM users WHERE id = ?";
let mut select = ok!(connection.prepare(select)).cursor();
let insert = "INSERT INTO users (id, name) VALUES (?, ?)";
let mut insert = ok!(connection.prepare(insert)).cursor();
for _ in 0..10 {
ok!(select.bind(&[Value::Integer(1)]));
assert_eq!(
ok!(ok!(select.next())),
&[Value::Integer(1), Value::String("Alice".to_string())]
);
assert_eq!(ok!(select.next()), None);
}
ok!(select.bind(&[Value::Integer(42)]));
assert_eq!(ok!(select.next()), None);
ok!(insert.bind(&[Value::Integer(42), Value::String("Bob".to_string())]));
assert_eq!(ok!(insert.next()), None);
ok!(select.bind(&[Value::Integer(42)]));
assert_eq!(
ok!(ok!(select.next())),
&[Value::Integer(42), Value::String("Bob".to_string())]
);
assert_eq!(ok!(select.next()), None);
}
#[test]
fn statement_bind() {
let connection = setup_users(":memory:");
let statement = "INSERT INTO users VALUES (?, ?, ?, ?, ?)";
let mut statement = ok!(connection.prepare(statement));
ok!(statement.bind(1, 2i64));
ok!(statement.bind(2, "Bob"));
ok!(statement.bind(3, 69.42));
ok!(statement.bind(4, &[0x69u8, 0x42u8][..]));
ok!(statement.bind(5, ()));
assert_eq!(ok!(statement.next()), State::Done);
}
#[test]
fn statement_bind_with_optional() {
let connection = setup_users(":memory:");
let statement = "INSERT INTO users VALUES (?, ?, ?, ?, ?)";
let mut statement = ok!(connection.prepare(statement));
ok!(statement.bind(1, None::<i64>));
ok!(statement.bind(2, None::<&str>));
ok!(statement.bind(3, None::<f64>));
ok!(statement.bind(4, None::<&[u8]>));
ok!(statement.bind(5, None::<&str>));
assert_eq!(ok!(statement.next()), State::Done);
let statement = "INSERT INTO users VALUES (?, ?, ?, ?, ?)";
let mut statement = ok!(connection.prepare(statement));
ok!(statement.bind(1, Some(2i64)));
ok!(statement.bind(2, Some("Bob")));
ok!(statement.bind(3, Some(69.42)));
ok!(statement.bind(4, Some(&[0x69u8, 0x42u8][..])));
ok!(statement.bind(5, None::<&str>));
assert_eq!(ok!(statement.next()), State::Done);
}
#[test]
fn statement_count() {
let connection = setup_users(":memory:");
let statement = "SELECT * FROM users";
let mut statement = ok!(connection.prepare(statement));
assert_eq!(ok!(statement.next()), State::Row);
assert_eq!(statement.count(), 5);
}
#[test]
fn statement_name() {
let connection = setup_users(":memory:");
let statement = "SELECT id, name, age, photo AS user_photo FROM users";
let statement = ok!(connection.prepare(statement));
let names = statement.names();
assert_eq!(names, vec!["id", "name", "age", "user_photo"]);
assert_eq!("user_photo", statement.name(3));
}
#[test]
fn statement_kind() {
let connection = setup_users(":memory:");
let statement = "SELECT * FROM users";
let mut statement = ok!(connection.prepare(statement));
assert_eq!(statement.kind(0), Type::Null);
assert_eq!(statement.kind(1), Type::Null);
assert_eq!(statement.kind(2), Type::Null);
assert_eq!(statement.kind(3), Type::Null);
assert_eq!(ok!(statement.next()), State::Row);
assert_eq!(statement.kind(0), Type::Integer);
assert_eq!(statement.kind(1), Type::String);
assert_eq!(statement.kind(2), Type::Float);
assert_eq!(statement.kind(3), Type::Binary);
}
#[test]
fn statement_read() {
let connection = setup_users(":memory:");
let statement = "SELECT * FROM users";
let mut statement = ok!(connection.prepare(statement));
assert_eq!(ok!(statement.next()), State::Row);
assert_eq!(ok!(statement.read::<i64>(0)), 1);
assert_eq!(ok!(statement.read::<String>(1)), String::from("Alice"));
assert_eq!(ok!(statement.read::<f64>(2)), 42.69);
assert_eq!(ok!(statement.read::<Vec<u8>>(3)), vec![0x42, 0x69]);
assert_eq!(ok!(statement.read::<Value>(4)), Value::Null);
assert_eq!(ok!(statement.next()), State::Done);
}
#[test]
fn statement_read_with_optional() {
let connection = setup_users(":memory:");
let statement = "SELECT * FROM users";
let mut statement = ok!(connection.prepare(statement));
assert_eq!(ok!(statement.next()), State::Row);
assert_eq!(ok!(statement.read::<Option<i64>>(0)), Some(1));
assert_eq!(
ok!(statement.read::<Option<String>>(1)),
Some(String::from("Alice"))
);
assert_eq!(ok!(statement.read::<Option<f64>>(2)), Some(42.69));
assert_eq!(
ok!(statement.read::<Option<Vec<u8>>>(3)),
Some(vec![0x42, 0x69])
);
assert_eq!(ok!(statement.read::<Option<String>>(4)), None);
assert_eq!(ok!(statement.next()), State::Done);
}
#[test]
fn statement_wildcard() {
let connection = setup_english(":memory:");
let statement = "SELECT value FROM english WHERE value LIKE '%type'";
let mut statement = ok!(connection.prepare(statement));
let mut count = 0;
while let State::Row = ok!(statement.next()) {
count += 1;
}
assert_eq!(count, 6);
}
#[test]
fn statement_wildcard_with_binding() {
let connection = setup_english(":memory:");
let statement = "SELECT value FROM english WHERE value LIKE ?";
let mut statement = ok!(connection.prepare(statement));
ok!(statement.bind(1, "%type"));
let mut count = 0;
while let State::Row = ok!(statement.next()) {
count += 1;
}
assert_eq!(count, 6);
}
fn setup_english<T: AsRef<Path>>(path: T) -> Connection {
let connection = ok!(sqlite::open(path));
ok!(connection.execute(
"
CREATE TABLE english (value TEXT);
INSERT INTO english VALUES ('cerotype');
INSERT INTO english VALUES ('metatype');
INSERT INTO english VALUES ('ozotype');
INSERT INTO english VALUES ('phenotype');
INSERT INTO english VALUES ('plastotype');
INSERT INTO english VALUES ('undertype');
INSERT INTO english VALUES ('nonsence');
",
));
connection
}
fn setup_users<T: AsRef<Path>>(path: T) -> Connection {
let connection = ok!(sqlite::open(path));
ok!(connection.execute(
"
CREATE TABLE users (id INTEGER, name TEXT, age REAL, photo BLOB, email TEXT);
INSERT INTO users VALUES (1, 'Alice', 42.69, X'4269', NULL);
",
));
connection
}

39
tests/tests.rs Normal file
View File

@ -0,0 +1,39 @@
/*
* Copyright 2021 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
mod tests {
use marine_rs_sdk_test::marine_test;
#[marine_test(config_path = "../Config.toml", modules_dir = "../artifacts/")]
fn test1(test: marine_test_env::test::ModuleInterface) {
test.test1()
}
#[marine_test(config_path = "../Config.toml", modules_dir = "../artifacts/")]
fn test2(test: marine_test_env::test::ModuleInterface) {
test.test2()
}
#[marine_test(config_path = "../Config.toml", modules_dir = "../artifacts/")]
fn test3(test: marine_test_env::test::ModuleInterface) {
test.test3()
}
#[marine_test(config_path = "../Config.toml", modules_dir = "../artifacts/")]
fn test4(test: marine_test_env::test::ModuleInterface) {
test.test4()
}
}