Skip to content

Commit

Permalink
Merge pull request #288 from NethermindEth/bohdan/scarb-test
Browse files Browse the repository at this point in the history
feat: add scarb-test
  • Loading branch information
varex83 authored Dec 30, 2024
2 parents 35fb472 + 459a3fa commit 322af6f
Show file tree
Hide file tree
Showing 56 changed files with 565 additions and 560 deletions.
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
File renamed without changes.
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
File renamed without changes.
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
File renamed without changes.
Loading

0 comments on commit 322af6f

Please sign in to comment.