Run Enso Cloud tests on the CI (#10964)

- Closes #9523

(cherry picked from commit 0543a69594)
This commit is contained in:
Radosław Waśko 2024-09-04 13:04:54 +02:00 committed by James Dunkerley
parent babddd768d
commit e81e98f835
9 changed files with 378 additions and 13 deletions

View File

@ -44,6 +44,11 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend test std-snowflake
env:
ENSO_CLOUD_COGNITO_REGION: ${{ secrets.ENSO_CLOUD_COGNITO_REGION }}
ENSO_CLOUD_COGNITO_USER_POOL_ID: ${{ secrets.ENSO_CLOUD_COGNITO_USER_POOL_ID }}
ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID: ${{ secrets.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID }}
ENSO_CLOUD_TEST_ACCOUNT_PASSWORD: ${{ secrets.ENSO_CLOUD_TEST_ACCOUNT_PASSWORD }}
ENSO_CLOUD_TEST_ACCOUNT_USERNAME: ${{ secrets.ENSO_CLOUD_TEST_ACCOUNT_USERNAME }}
ENSO_SNOWFLAKE_ACCOUNT: ${{ secrets.ENSO_SNOWFLAKE_ACCOUNT }}
ENSO_SNOWFLAKE_DATABASE: ${{ secrets.ENSO_SNOWFLAKE_DATABASE }}
ENSO_SNOWFLAKE_PASSWORD: ${{ secrets.ENSO_SNOWFLAKE_PASSWORD }}
@ -75,5 +80,69 @@ jobs:
GRAAL_EDITION: GraalVM CE
permissions:
checks: write
enso-build-ci-gen-job-standard-library-tests-graal-vm-ce-linux-amd64:
name: Standard Library Tests (GraalVM CE) (linux, amd64)
runs-on:
- self-hosted
- Linux
steps:
- if: startsWith(runner.name, 'GitHub Actions') || startsWith(runner.name, 'Hosted Agent')
name: Installing wasm-pack
uses: jetli/wasm-pack-action@v0.4.0
with:
version: v0.10.2
- name: Expose Artifact API and context information.
uses: actions/github-script@v7
with:
script: "\n core.exportVariable(\"ACTIONS_RUNTIME_TOKEN\", process.env[\"ACTIONS_RUNTIME_TOKEN\"])\n core.exportVariable(\"ACTIONS_RUNTIME_URL\", process.env[\"ACTIONS_RUNTIME_URL\"])\n core.exportVariable(\"GITHUB_RETENTION_DAYS\", process.env[\"GITHUB_RETENTION_DAYS\"])\n console.log(context)\n "
- name: Checking out the repository
uses: actions/checkout@v4
with:
clean: false
submodules: recursive
- name: Build Script Setup
run: ./run --help || (git clean -ffdx && ./run --help)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: "(contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean before
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: ./run backend test standard-library
env:
ENSO_CLOUD_COGNITO_REGION: ${{ secrets.ENSO_CLOUD_COGNITO_REGION }}
ENSO_CLOUD_COGNITO_USER_POOL_ID: ${{ secrets.ENSO_CLOUD_COGNITO_USER_POOL_ID }}
ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID: ${{ secrets.ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID }}
ENSO_CLOUD_TEST_ACCOUNT_PASSWORD: ${{ secrets.ENSO_CLOUD_TEST_ACCOUNT_PASSWORD }}
ENSO_CLOUD_TEST_ACCOUNT_USERNAME: ${{ secrets.ENSO_CLOUD_TEST_ACCOUNT_USERNAME }}
ENSO_LIB_S3_AWS_ACCESS_KEY_ID: ${{ secrets.ENSO_LIB_S3_AWS_ACCESS_KEY_ID }}
ENSO_LIB_S3_AWS_REGION: ${{ secrets.ENSO_LIB_S3_AWS_REGION }}
ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY: ${{ secrets.ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository
name: Standard Library Test Reporter
uses: dorny/test-reporter@v1
with:
max-annotations: 50
name: Standard Library Tests Report (GraalVM CE, linux, amd64)
path: ${{ env.ENSO_TEST_JUNIT_DIR }}/*/*.xml
path-replace-backslashes: true
reporter: java-junit
- if: failure() && runner.os == 'Windows'
name: List files if failed (Windows)
run: Get-ChildItem -Force -Recurse
- if: failure() && runner.os != 'Windows'
name: List files if failed (non-Windows)
run: ls -lAR
- if: "(always()) && (contains(github.event.pull_request.labels.*.name, 'CI: Clean build required') || inputs.clean_build_required)"
name: Clean after
run: ./run git-clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
env:
GRAAL_EDITION: GraalVM CE
permissions:
checks: write
env:
ENSO_BUILD_SKIP_VERSION_CHECK: "true"

View File

@ -1,6 +1,6 @@
{
"rust-analyzer.linkedProjects": [
"./app/gui2/rust-ffi/Cargo.toml"
"./app/rust-ffi/Cargo.toml"
],
"vue.complete.casing.status": false,
"vue.complete.casing.props": "camel",
@ -19,7 +19,7 @@
},
"eslint.workingDirectories": [
"./app/gui2",
"./app/gui2/ide-desktop"
"./app/ide-desktop"
],
"files.watcherExclude": {
"**/target": true

View File

@ -111,6 +111,13 @@ pub mod secret {
/// Static token for admin requests on our Lambdas.
pub const ENSO_ADMIN_TOKEN: &str = "ENSO_ADMIN_TOKEN";
// === Enso Cloud Test Account ===
pub const ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID: &str =
"ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID";
pub const ENSO_CLOUD_COGNITO_USER_POOL_ID: &str = "ENSO_CLOUD_COGNITO_USER_POOL_ID";
pub const ENSO_CLOUD_COGNITO_REGION: &str = "ENSO_CLOUD_COGNITO_REGION";
pub const ENSO_CLOUD_TEST_ACCOUNT_USERNAME: &str = "ENSO_CLOUD_TEST_ACCOUNT_USERNAME";
pub const ENSO_CLOUD_TEST_ACCOUNT_PASSWORD: &str = "ENSO_CLOUD_TEST_ACCOUNT_PASSWORD";
// === Apple Code Signing & Notarization ===
pub const APPLE_CODE_SIGNING_CERT: &str = "APPLE_CODE_SIGNING_CERT";
@ -538,7 +545,7 @@ pub fn add_backend_checks(
) {
workflow.add(target, job::CiCheckBackend { graal_edition });
workflow.add(target, job::JvmTests { graal_edition });
workflow.add(target, job::StandardLibraryTests { graal_edition });
workflow.add(target, job::StandardLibraryTests { graal_edition, cloud_tests_enabled: false });
}
pub fn workflow_call_job(name: impl Into<String>, path: impl Into<String>) -> Job {
@ -702,6 +709,10 @@ pub fn extra_nightly_tests() -> Result<Workflow> {
// behavior.
let target = (OS::Linux, Arch::X86_64);
workflow.add(target, job::SnowflakeTests {});
workflow.add(target, job::StandardLibraryTests {
graal_edition: graalvm::Edition::Community,
cloud_tests_enabled: true,
});
Ok(workflow)
}

View File

@ -211,14 +211,39 @@ impl JobArchetype for JvmTests {
}
}
fn enable_cloud_tests(step: Step) -> Step {
step.with_secret_exposed_as(
secret::ENSO_CLOUD_COGNITO_USER_POOL_ID,
crate::cloud_tests::env::ci_config::ENSO_CLOUD_COGNITO_USER_POOL_ID,
)
.with_secret_exposed_as(
secret::ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID,
crate::cloud_tests::env::ci_config::ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID,
)
.with_secret_exposed_as(
secret::ENSO_CLOUD_COGNITO_REGION,
crate::cloud_tests::env::ci_config::ENSO_CLOUD_COGNITO_REGION,
)
.with_secret_exposed_as(
secret::ENSO_CLOUD_TEST_ACCOUNT_USERNAME,
crate::cloud_tests::env::ci_config::ENSO_CLOUD_TEST_ACCOUNT_USERNAME,
)
.with_secret_exposed_as(
secret::ENSO_CLOUD_TEST_ACCOUNT_PASSWORD,
crate::cloud_tests::env::ci_config::ENSO_CLOUD_TEST_ACCOUNT_PASSWORD,
)
}
#[derive(Clone, Copy, Debug)]
pub struct StandardLibraryTests {
pub graal_edition: graalvm::Edition,
pub graal_edition: graalvm::Edition,
pub cloud_tests_enabled: bool,
}
impl JobArchetype for StandardLibraryTests {
fn job(&self, target: Target) -> Job {
let graal_edition = self.graal_edition;
let should_enable_cloud_tests = self.cloud_tests_enabled;
let job_name = format!("Standard Library Tests ({graal_edition})");
let mut job = RunStepsBuilder::new("backend test standard-library")
.customize(move |step| {
@ -235,7 +260,14 @@ impl JobArchetype for StandardLibraryTests {
secret::ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY,
crate::libraries_tests::s3::env::ENSO_LIB_S3_AWS_SECRET_ACCESS_KEY,
);
vec![main_step, step::stdlib_test_reporter(target, graal_edition)]
let updated_main_step = if should_enable_cloud_tests {
enable_cloud_tests(main_step)
} else {
main_step
};
vec![updated_main_step, step::stdlib_test_reporter(target, graal_edition)]
})
.build_job(job_name, target)
.with_permission(Permission::Checks, Access::Write);
@ -291,8 +323,11 @@ impl JobArchetype for SnowflakeTests {
secret::ENSO_SNOWFLAKE_WAREHOUSE,
crate::libraries_tests::snowflake::env::ENSO_SNOWFLAKE_WAREHOUSE,
);
let updated_main_step = enable_cloud_tests(main_step);
vec![
main_step,
updated_main_step,
step::extra_stdlib_test_reporter(target, GRAAL_EDITION_FOR_EXTRA_TESTS),
]
})

View File

@ -0,0 +1,39 @@
//! Environment variables commonly used by AWS services.
use ide_ci::define_env_var;
pub mod ci_config {
use super::*;
define_env_var! {
/// Username for an Enso Cloud account used for running Cloud integration tests.
ENSO_CLOUD_TEST_ACCOUNT_USERNAME, String;
/// Password for an Enso Cloud account used for running Cloud integration tests.
ENSO_CLOUD_TEST_ACCOUNT_PASSWORD, String;
// The Client ID of the User Pool for Enso Cloud Cognito auth flow.
ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID, String;
// The User Pool ID for Enso Cloud Cognito auth flow.
ENSO_CLOUD_COGNITO_USER_POOL_ID, String;
// The Region used for Cognito auth flow.
ENSO_CLOUD_COGNITO_REGION, String;
}
}
pub mod test_controls {
use super::*;
define_env_var! {
/// Locates an Enso Cloud credentials file used in tests.
ENSO_CLOUD_CREDENTIALS_FILE, String;
/// Denotes the URI of the Enso Cloud API deployment to be used in tests.
ENSO_CLOUD_API_URI, String;
/// A flag that tells the test suite to run applicable tests on the cloud environment instead of just a mock.
ENSO_RUN_REAL_CLOUD_TEST, String;
}
}

View File

@ -0,0 +1,166 @@
//! Module that allows to create an Enso Cloud compatible credentials file from
//! a configuration stored in environment variables.
pub mod env;
use anyhow::Ok;
use tempfile::NamedTempFile;
use crate::prelude::*;
use std::fs::File;
use std::io::Write;
pub fn build_auth_config_from_environment() -> Result<AuthConfig> {
let web_client_id = env::ci_config::ENSO_CLOUD_COGNITO_USER_POOL_WEB_CLIENT_ID.get()?;
let pool_id = env::ci_config::ENSO_CLOUD_COGNITO_USER_POOL_ID.get()?;
let region = env::ci_config::ENSO_CLOUD_COGNITO_REGION.get()?;
let username = env::ci_config::ENSO_CLOUD_TEST_ACCOUNT_USERNAME.get()?;
let password = env::ci_config::ENSO_CLOUD_TEST_ACCOUNT_PASSWORD.get()?;
Ok(AuthConfig { web_client_id, user_pool_id: pool_id, region, username, password })
}
pub async fn prepare_credentials_file(auth_config: AuthConfig) -> Result<NamedTempFile> {
let credentials = build_credentials(auth_config).await?;
let credentials_temp_file = NamedTempFile::with_prefix("enso-cloud-credentials")?;
save_credentials(&credentials, credentials_temp_file.path())?;
Ok(credentials_temp_file)
}
#[derive(Debug)]
pub struct AuthConfig {
web_client_id: String,
user_pool_id: String,
region: String,
username: String,
password: String,
}
struct Credentials {
client_id: String,
access_token: String,
refresh_token: String,
refresh_url: String,
expire_at: String,
}
async fn build_credentials(config: AuthConfig) -> Result<Credentials> {
if !is_aws_cli_installed().await {
return Err(anyhow!("AWS CLI is not installed. If you want the build script to generate the Enso Cloud credentials file, you must install the AWS CLI."));
}
// We save the timestamp before the authentication, as it's better to say the token expires a
// bit earlier than to make it expire later than in reality and make downstream user mistakenly
// use an expired token.
let now_before_auth = chrono::Utc::now();
let mut command = aws_command();
command
.args(["cognito-idp", "initiate-auth"])
.args(["--region", &config.region])
.args(["--auth-flow", "USER_PASSWORD_AUTH"])
.args([
"--auth-parameters",
&format!("USERNAME={},PASSWORD={}", config.username, config.password),
])
.args(["--client-id", &config.web_client_id]);
let stdout = command.run_stdout().await?;
let cognito_response = parse_cognito_response(&stdout)?;
let expire_at = now_before_auth + chrono::Duration::seconds(cognito_response.expires_in);
let expire_at_str = expire_at.to_rfc3339();
let refresh_url =
format!("https://cognito-idp.{}.amazonaws.com/{}", config.region, config.user_pool_id);
Ok(Credentials {
client_id: config.web_client_id.to_string(),
access_token: cognito_response.access_token,
refresh_token: cognito_response.refresh_token,
expire_at: expire_at_str,
refresh_url,
})
}
async fn is_aws_cli_installed() -> bool {
let mut command = aws_command();
command.arg("--version");
command.run_ok().await.is_ok()
}
fn aws_command() -> Command {
Command::new("aws")
}
struct CognitoResponse {
access_token: String,
refresh_token: String,
expires_in: i64,
}
fn parse_cognito_response(response: &str) -> Result<CognitoResponse> {
let json: serde_json::Value = serde_json::from_str(response)?;
let root_mapping = unpack_object(&json)?;
let authentication_result_mapping =
unpack_object(get_or_fail(root_mapping, "AuthenticationResult")?)?;
let token_type = unpack_string(get_or_fail(authentication_result_mapping, "TokenType")?)?;
if token_type != "Bearer" {
return Err(anyhow!("Expected token type 'Bearer', but got: {}", token_type));
}
let access_token = unpack_string(get_or_fail(authentication_result_mapping, "AccessToken")?)?;
let refresh_token = unpack_string(get_or_fail(authentication_result_mapping, "RefreshToken")?)?;
let expires_in = unpack_integer(get_or_fail(authentication_result_mapping, "ExpiresIn")?)?;
Ok(CognitoResponse {
access_token: access_token.to_string(),
refresh_token: refresh_token.to_string(),
expires_in,
})
}
fn get_or_fail<'a>(
mapping: &'a serde_json::Map<String, serde_json::Value>,
key: &str,
) -> Result<&'a serde_json::Value> {
match mapping.get(key) {
Some(value) => Ok(value),
None => Err(anyhow!("Missing key when deserializing JSON: {}", key)),
}
}
fn unpack_object(value: &serde_json::Value) -> Result<&serde_json::Map<String, serde_json::Value>> {
if let serde_json::Value::Object(mapping) = value {
Ok(mapping)
} else {
Err(anyhow!("Expected JSON object, but got: {:?}", value))
}
}
fn unpack_string(value: &serde_json::Value) -> Result<&String> {
if let serde_json::Value::String(string) = value {
Ok(string)
} else {
Err(anyhow!("Expected JSON string, but got: {:?}", value))
}
}
fn unpack_integer(value: &serde_json::Value) -> Result<i64> {
if let serde_json::Value::Number(number) = value {
Ok(number.as_i64().ok_or_else(|| anyhow!("Expected JSON integer, but got: {:?}", value))?)
} else {
Err(anyhow!("Expected JSON integer, but got: {:?}", value))
}
}
fn save_credentials(credentials: &Credentials, path: &Path) -> Result<()> {
let json = serde_json::json! {
{
"client_id": credentials.client_id,
"access_token": credentials.access_token,
"refresh_token": credentials.refresh_token,
"refresh_url": credentials.refresh_url,
"expire_at": credentials.expire_at,
}
};
let mut file = File::create(path)?;
file.write_all(json.to_string().as_bytes())?;
Ok(())
}

View File

@ -1,3 +1,4 @@
use crate::cloud_tests;
use crate::prelude::*;
use crate::engine::StandardLibraryTestsSelection;
@ -13,6 +14,7 @@ use crate::sqlserver;
use crate::sqlserver::EndpointConfiguration as SQLServerEndpointConfiguration;
use crate::sqlserver::SQLServer;
use ide_ci::env::accessor::TypedVariable;
use ide_ci::future::AsyncPolicy;
use ide_ci::programs::docker::ContainerId;
@ -96,7 +98,12 @@ impl BuiltEnso {
benchmarks
}
pub fn run_test(&self, test_path: impl AsRef<Path>, ir_caches: IrCaches) -> Result<Command> {
pub fn run_test(
&self,
test_path: impl AsRef<Path>,
ir_caches: IrCaches,
environment_overrides: Vec<(String, String)>,
) -> Result<Command> {
let mut command = self.cmd()?;
let base_working_directory = test_path.try_parent()?;
command
@ -107,6 +114,11 @@ impl BuiltEnso {
// This flag enables assertions in the JVM. Some of our stdlib tests had in the past
// failed on Graal/Truffle assertions, so we want to have them triggered.
.set_env(JAVA_OPTS, &ide_ci::programs::java::Option::EnableAssertions.as_ref())?;
for (k, v) in environment_overrides {
command.env(k, &v);
}
if test_path.as_str().contains("_Internal_") {
command.arg("--disable-private-check");
}
@ -160,6 +172,18 @@ impl BuiltEnso {
only.iter().any(|test| test.contains("Microsoft_Tests")),
};
let cloud_credentials_file = match cloud_tests::build_auth_config_from_environment() {
Ok(config) => {
let file = cloud_tests::prepare_credentials_file(config).await?;
info!("Enso Cloud authentication (for cloud integration tests) is enabled.");
Some(file)
}
Err(err) => {
info!("Enso Cloud authentication (for cloud integration tests) is skipped, because of: {}", err);
None
}
};
let _httpbin = crate::httpbin::get_and_spawn_httpbin_on_free_port(sbt).await?;
let _postgres = match TARGET_OS {
@ -210,8 +234,25 @@ impl BuiltEnso {
_ => None,
};
let mut environment_overrides: Vec<(String, String)> = vec![];
if let Some(credentials_file) = cloud_credentials_file.as_ref() {
let path_as_str = credentials_file.path().to_str();
let path = path_as_str
.ok_or_else(|| anyhow!("Path to credentials file is not valid UTF-8"))?;
environment_overrides.push((
cloud_tests::env::test_controls::ENSO_CLOUD_CREDENTIALS_FILE.name().to_string(),
path.to_string(),
));
// We do not set ENSO_CLOUD_API_URI - we rely on the default, or any existing overrides.
environment_overrides.push((
cloud_tests::env::test_controls::ENSO_RUN_REAL_CLOUD_TEST.name().to_string(),
"1".to_string(),
));
};
let futures = std_tests.into_iter().map(|test_path| {
let command = self.run_test(test_path, ir_caches);
let command: std::result::Result<Command, anyhow::Error> =
self.run_test(test_path, ir_caches, environment_overrides.clone());
async move { command?.run_ok().await }
});
@ -219,6 +260,8 @@ impl BuiltEnso {
// Could share them with Arc but then scenario of multiple test runs being run in parallel
// should be handled, e.g. avoiding port collisions.
let results = ide_ci::future::join_all(futures, async_policy).await;
// Only drop the credentials file after all tests have finished.
drop(cloud_credentials_file);
let errors = results.into_iter().filter_map(Result::err).collect::<Vec<_>>();
if errors.is_empty() {
Ok(())

View File

@ -21,6 +21,7 @@ pub mod aws;
pub mod changelog;
pub mod ci;
pub mod ci_gen;
pub mod cloud_tests;
pub mod config;
pub mod context;
pub mod engine;

View File

@ -366,12 +366,12 @@ add_specs suite_builder =
current_project_root = enso_project.root
base_directory = current_project_root.parent
is_correct_working_directory = (File.current_directory . normalize . path) == current_project_root.absolute.normalize.path
is_correct_working_directory = File.current_directory.absolute.normalize.path == base_directory.absolute.normalize.path
group_builder.specify "will resolve relative paths relative to the currently running project" pending=(if is_correct_working_directory.not then "The working directory is not set-up as expected, so this test cannot run. Please run the tests using `ensoup` to ensure the working directory is correct.") <|
root = File.new "."
root.should_be_a File
dot = File.new "."
dot.should_be_a File
# The `.` path should resolve to the base path
root.absolute.normalize.path . should_equal base_directory.absolute.normalize.path
dot.absolute.normalize.path . should_equal base_directory.absolute.normalize.path
expected_file = base_directory / "abc" / "def.txt"
f = File.new "abc/def.txt"
@ -396,7 +396,8 @@ add_specs suite_builder =
Test_Environment.unsafe_with_environment_override "ENSO_CLOUD_PROJECT_DIRECTORY_PATH" subdir.path <|
# Flush caches to ensure fresh dir is used
Enso_User.flush_caches
action
Test.with_clue "(running with ENSO_CLOUD_PROJECT_DIRECTORY_PATH set to "+subdir.path+") " <|
action
group_builder.specify "will resolve relative paths as Cloud paths if running in the Cloud" pending=cloud_setup.real_cloud_pending <|
with_temporary_cloud_root <|