Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add scarb-test #288

Merged
merged 4 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 5 additions & 49 deletions api/src/handlers/compile.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use crate::errors::{CmdError, ExecutionError, FileError, Result};
use crate::handlers::allowed_versions::is_version_allowed;
use crate::handlers::process::{do_process_command, fetch_process_result};
use crate::handlers::types::CompileResponse;
use crate::handlers::types::{ApiCommand, CompileResponseGetter, IntoTypedResponse};
use crate::handlers::types::{CompileResponse, FileContentMap};
use crate::handlers::utils::{get_files_recursive, init_directories, AutoCleanUp};
use crate::handlers::utils::{
ensure_scarb_toml, get_files_recursive, init_directories, is_single_file_compilation,
AutoCleanUp,
};
use crate::metrics::Metrics;
use crate::rate_limiter::RateLimited;
use crate::worker::WorkerEngine;
Expand Down Expand Up @@ -61,53 +64,6 @@ pub async fn get_compile_result(process_id: &str, engine: &State<WorkerEngine>)
.unwrap_or_else(|err| err.into_typed())
}

async fn ensure_scarb_toml(
mut compilation_request: CompilationRequest,
) -> Result<CompilationRequest> {
// Check if Scarb.toml exists in the root
if !compilation_request.has_scarb_toml() {
// number of files cairo files in the request
let cairo_files_count = compilation_request
.files
.iter()
.filter(|f| f.file_name.ends_with(".cairo"))
.count();

if cairo_files_count != 1 {
error!(
"Invalid request: Expected exactly one Cairo file, found {}",
cairo_files_count
);
return Err(ExecutionError::InvalidRequest.into());
}

tracing::debug!("No Scarb.toml found, creating default one");
compilation_request.files.push(FileContentMap {
file_name: "Scarb.toml".to_string(),
file_content: match compilation_request.version {
Some(ref version) => scarb_toml_with_version(version),
None => default_scarb_toml(),
},
});

// change the name of the file to the first cairo file to src/lib.cairo
if let Some(first_cairo_file) = compilation_request
.files
.iter_mut()
.find(|f| f.file_name.ends_with(".cairo"))
{
first_cairo_file.file_name = "src/lib.cairo".to_string();
}
}

Ok(compilation_request)
}

fn is_single_file_compilation(compilation_request: &CompilationRequest) -> bool {
compilation_request.files.len() == 1
&& compilation_request.files[0].file_name.ends_with(".cairo")
}

/// Run Scarb to compile a project
///
/// # Errors
Expand Down
67 changes: 32 additions & 35 deletions api/src/handlers/scarb_test.rs
Original file line number Diff line number Diff line change
@@ -1,35 +1,39 @@
use crate::errors::{CmdError, FileError, Result, SystemError};
use super::types::ApiResponse;
use crate::errors::{CmdError, FileError, Result};
use crate::handlers::process::{do_process_command, fetch_process_result};
use crate::handlers::types::TestResponseGetter;
use crate::handlers::types::{ApiCommand, IntoTypedResponse, TestResponse};
use crate::handlers::types::{TestRequest, TestResponseGetter};
use crate::handlers::utils::{init_directories, AutoCleanUp};
use crate::rate_limiter::RateLimited;
use crate::utils::lib::get_file_path;
use crate::worker::WorkerEngine;
use rocket::serde::json::Json;
use rocket::State;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use tracing::{debug, error, info, instrument};

use super::types::ApiResponse;

#[instrument(skip(engine, _rate_limited))]
#[post("/scarb-test-async/<remix_file_path..>")]
#[instrument(skip(test_request, engine, _rate_limited))]
#[post("/test-async", data = "<test_request>")]
pub async fn scarb_test_async(
remix_file_path: PathBuf,
test_request: Json<TestRequest>,
engine: &State<WorkerEngine>,
_rate_limited: RateLimited,
) -> ApiResponse<String> {
info!("/scarb-test-async/{:?}", remix_file_path);
do_process_command(ApiCommand::ScarbTest { remix_file_path }, engine)
info!("/test-async");
do_process_command(
ApiCommand::ScarbTest {
test_request: test_request.0,
},
engine,
)
}

#[instrument(skip(engine))]
#[get("/scarb-test-async/<process_id>")]
#[get("/test-async/<process_id>")]
pub async fn get_scarb_test_result(
process_id: &str,
engine: &State<WorkerEngine>,
) -> ApiResponse<()> {
info!("/scarb-test-async/{:?}", process_id);
info!("/test-async/{:?}", process_id);
fetch_process_result::<TestResponseGetter>(process_id, engine)
.map(|result| result.0)
.unwrap_or_else(|err| err.into_typed())
Expand All @@ -44,21 +48,21 @@ pub async fn get_scarb_test_result(
/// - Failed to read command output
/// - Failed to parse command output as UTF-8
/// - Command returned non-zero status
pub async fn do_scarb_test(remix_file_path: PathBuf) -> Result<TestResponse> {
let remix_file_path = remix_file_path
.to_str()
.ok_or_else(|| {
error!("Failed to parse remix file path: {:?}", remix_file_path);
SystemError::FailedToParseFilePath(format!("{:?}", remix_file_path))
})?
.to_string();
pub async fn do_scarb_test(test_request: TestRequest) -> Result<TestResponse> {
// Create temporary directories
let temp_dir = init_directories(test_request).await.map_err(|e| {
error!("Failed to initialize directories: {:?}", e);
e
})?;

let file_path = get_file_path(&remix_file_path);
let auto_clean_up = AutoCleanUp {
dirs: vec![&temp_dir],
};

let mut compile = Command::new("scarb");
compile.current_dir(&file_path);
compile.current_dir(&temp_dir);

debug!("Executing scarb test command in directory: {:?}", file_path);
debug!("Executing scarb test command in directory: {:?}", temp_dir);

let result = compile
.arg("test")
Expand All @@ -77,15 +81,6 @@ pub async fn do_scarb_test(remix_file_path: PathBuf) -> Result<TestResponse> {
CmdError::FailedToReadOutput(e)
})?;

// Convert file path to string once to avoid repetition and potential inconsistencies
let file_path_str = file_path
.to_str()
.ok_or_else(|| {
error!("Failed to convert file path to string: {:?}", file_path);
SystemError::FailedToParseFilePath(format!("{:?}", file_path))
})?
.to_string();

let stdout = String::from_utf8(output.stdout).map_err(|e| {
error!("Failed to parse stdout as UTF-8: {:?}", e);
FileError::UTF8Error(e)
Expand All @@ -98,8 +93,8 @@ pub async fn do_scarb_test(remix_file_path: PathBuf) -> Result<TestResponse> {

let message = format!(
"{}{}",
stdout.replace(&file_path_str, &remix_file_path),
stderr.replace(&file_path_str, &remix_file_path)
stderr.replace(&temp_dir, ""),
stdout.replace(&temp_dir, "")
);

let (status, code) = match output.status.code() {
Expand All @@ -114,6 +109,8 @@ pub async fn do_scarb_test(remix_file_path: PathBuf) -> Result<TestResponse> {
}
};

auto_clean_up.clean_up_sync();

Ok(ApiResponse::ok(())
.with_status(status.to_string())
.with_code(code)
Expand Down
5 changes: 3 additions & 2 deletions api/src/handlers/types.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use rocket::http::{ContentType, Status};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use crate::errors::{ApiError, ExecutionError};

Expand Down Expand Up @@ -169,6 +168,8 @@ pub struct CompilationRequest {
pub version: Option<String>,
}

pub type TestRequest = CompilationRequest;

impl CompilationRequest {
pub fn has_scarb_toml(&self) -> bool {
self.files
Expand All @@ -188,7 +189,7 @@ pub enum ApiCommand {
compilation_request: CompilationRequest,
},
ScarbTest {
remix_file_path: PathBuf,
test_request: TestRequest,
},
#[allow(dead_code)]
Shutdown,
Expand Down
52 changes: 50 additions & 2 deletions api/src/handlers/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use std::time::Instant;
use std::{future::Future, path::PathBuf};
use tracing::{info, instrument};

use crate::errors::{ApiError, FileError, Result, SystemError};
use crate::errors::{ApiError, ExecutionError, FileError, Result, SystemError};
use crate::handlers::compile::{default_scarb_toml, scarb_toml_with_version};
use crate::metrics::{Metrics, COMPILATION_LABEL_VALUE};

use super::scarb_version::do_scarb_version;
Expand Down Expand Up @@ -112,7 +113,7 @@ pub async fn dispatch_command(command: ApiCommand, metrics: &Metrics) -> Result<
Err(e) => Err(e),
},
ApiCommand::Shutdown => Ok(ApiCommandResult::Shutdown),
ApiCommand::ScarbTest { remix_file_path } => match do_scarb_test(remix_file_path).await {
ApiCommand::ScarbTest { test_request } => match do_scarb_test(test_request).await {
Ok(result) => Ok(ApiCommandResult::Test(result)),
Err(e) => Err(e),
},
Expand Down Expand Up @@ -201,3 +202,50 @@ pub fn get_files_recursive(base_path: &Path) -> Result<Vec<FileContentMap>> {

Ok(file_content_map_array)
}

pub async fn ensure_scarb_toml(
mut compilation_request: CompilationRequest,
) -> Result<CompilationRequest> {
// Check if Scarb.toml exists in the root
if !compilation_request.has_scarb_toml() {
// number of files cairo files in the request
let cairo_files_count = compilation_request
.files
.iter()
.filter(|f| f.file_name.ends_with(".cairo"))
.count();

if cairo_files_count != 1 {
tracing::error!(
"Invalid request: Expected exactly one Cairo file, found {}",
cairo_files_count
);
return Err(ExecutionError::InvalidRequest.into());
}

tracing::debug!("No Scarb.toml found, creating default one");
compilation_request.files.push(FileContentMap {
file_name: "Scarb.toml".to_string(),
file_content: match compilation_request.version {
Some(ref version) => scarb_toml_with_version(version),
None => default_scarb_toml(),
},
});

// change the name of the file to the first cairo file to src/lib.cairo
if let Some(first_cairo_file) = compilation_request
.files
.iter_mut()
.find(|f| f.file_name.ends_with(".cairo"))
{
first_cairo_file.file_name = "src/lib.cairo".to_string();
}
}

Ok(compilation_request)
}

pub fn is_single_file_compilation(compilation_request: &CompilationRequest) -> bool {
compilation_request.files.len() == 1
&& compilation_request.files[0].file_name.ends_with(".cairo")
}
2 changes: 1 addition & 1 deletion plugin/src/components/BackgroundNotices/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { nanoid } from "nanoid";
import React from "react";
import "./style.css";
import "./styles.css";

const Notices = [
"The starknet Remix Plugin is in Alpha",
Expand Down
11 changes: 7 additions & 4 deletions plugin/src/components/Card/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { type ReactNode } from "react";
import React from "react";
import "./card.css";
import React, { type ReactNode } from "react";
import "./styles.css";

export interface CardProps {
header?: string;
rightItem?: ReactNode;
children: ReactNode;
}

export const Card: React.FC<CardProps> = ({ header, children, rightItem }) => {
export const Card: React.FC<CardProps> = ({
header,
children,
rightItem
}) => {
return (
<div className="border-top border-bottom">
{header !== undefined && (
Expand Down
7 changes: 5 additions & 2 deletions plugin/src/components/CurrentEnv/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import "./currentEnv.css";
import "./styles.css";
import { useAtomValue } from "jotai";
import { envAtom, envName, selectedDevnetAccountAtom } from "../../atoms/environment";
import { selectedAccountAtom } from "../../atoms/manualAccount";
Expand All @@ -18,7 +18,10 @@ export const CurrentEnv: React.FC = () => {

const selectedAccount =
env === "wallet"
? { address: walletAccount?.address, balance: 0 }
? {
address: walletAccount?.address,
balance: 0
}
: env === "manual"
? {
address: selectedAccountManual?.address,
Expand Down
19 changes: 13 additions & 6 deletions plugin/src/components/DevnetAccountSelector/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { getRoundedNumber, getShortenedHash, weiToEth } from "../../utils/utils"
import React, { useState } from "react";
import { Account, RpcProvider } from "starknet";
import { MdCheck, MdCopyAll } from "react-icons/md";
import "./devnetAccountSelector.css";
import "./styles.css";
import copy from "copy-to-clipboard";
import { useAtom, useAtomValue } from "jotai";
import {
Expand All @@ -18,8 +18,14 @@ import { BsCheck, BsChevronDown } from "react-icons/bs";
import * as Select from "../../components/ui_components/Select";

const DevnetAccountSelector: React.FC = () => {
const { account, setAccount } = useAccount();
const { provider, setProvider } = useProvider();
const {
account,
setAccount
} = useAccount();
const {
provider,
setProvider
} = useProvider();
const env = useAtomValue(envAtom);
const devnet = useAtomValue(devnetAtom);
const isDevnetAlive = useAtomValue(isDevnetAliveAtom);
Expand All @@ -29,7 +35,7 @@ const DevnetAccountSelector: React.FC = () => {
const [showCopied, setCopied] = useState(false);
const [accountIdx, setAccountIdx] = useState(0);

function handleAccountChange(value: number): void {
function handleAccountChange (value: number): void {
if (value === -1) {
return;
}
Expand All @@ -56,7 +62,8 @@ const DevnetAccountSelector: React.FC = () => {
handleAccountChange(parseInt(value));
}}
>
<Select.Trigger className="flex flex-row justify-content-space-between align-items-center p-2 br-1 devnet-account-selector-trigger">
<Select.Trigger
className="flex flex-row justify-content-space-between align-items-center p-2 br-1 devnet-account-selector-trigger">
<Select.Value placeholder="No accounts found">
{availableDevnetAccounts !== undefined &&
availableDevnetAccounts.length !== 0 &&
Expand Down Expand Up @@ -96,7 +103,7 @@ const DevnetAccountSelector: React.FC = () => {
)
: (
<Select.Item value="-1" key={-1}>
No accounts found
No accounts found
</Select.Item>
)}
</Select.Viewport>
Expand Down
5 changes: 2 additions & 3 deletions plugin/src/components/EnvCard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/* eslint-disable react/prop-types */
import { type DisconnectOptions } from "get-starknet";
import { type ReactNode, useState } from "react";
import React from "react";
import "./envCard.css";
import React, { type ReactNode, useState } from "react";
import "./styles.css";
import { useAtomValue } from "jotai";
import { envAtom } from "../../atoms/environment";

Expand Down
Loading
Loading