@@ -427,7 +427,7 @@ fn run_configuration_generation_uses_a_maven_project_below_the_workspace() {
427427 . filter_map( Value :: as_str)
428428 . any( |argument| argument == "-am" || argument == "spring-boot:run" ) ) ;
429429
430- fs:: create_dir_all ( root. join ( "custom-run/service " ) ) . unwrap ( ) ;
430+ fs:: create_dir_all ( root. join ( "custom-run" ) ) . unwrap ( ) ;
431431 fs:: write (
432432 root. join ( ".lithe/run/configurations.json" ) ,
433433 serde_json:: json!( {
@@ -3624,3 +3624,227 @@ fn an_unknown_java_entrypoint_schema_is_rejected() {
36243624 assert_eq ! ( response[ "ok" ] , false , "{response}" ) ;
36253625 fs:: remove_dir_all ( root) . unwrap ( ) ;
36263626}
3627+
3628+ fn run_config_command ( command : & str , payload : Value ) -> Value {
3629+ serde_json:: from_str ( & execute_json (
3630+ & serde_json:: json!( { "id" : command, "command" : command, "payload" : payload } ) . to_string ( ) ,
3631+ ) )
3632+ . unwrap ( )
3633+ }
3634+
3635+ /// Issue #861: a multi-module Spring Boot service whose working directory is
3636+ /// set to its own module must stay in the run list. `maven.module` is relative
3637+ /// to the owning reactor, so resolving it against the overridden `cwd` looked
3638+ /// for `shop-web/shop-web` and silently dropped the entry.
3639+ #[ test]
3640+ fn working_directory_override_keeps_reactor_module_configurations ( ) {
3641+ let root = temporary_root ( "run-config-module-cwd" ) ;
3642+ // The guard also removes the workspace when an assertion fails.
3643+ struct Cleanup ( PathBuf ) ;
3644+ impl Drop for Cleanup {
3645+ fn drop ( & mut self ) {
3646+ let _ = fs:: remove_dir_all ( & self . 0 ) ;
3647+ }
3648+ }
3649+ let _cleanup = Cleanup ( root. clone ( ) ) ;
3650+ let source = "shop-web/src/main/java/com/example/WebApp.java" ;
3651+ fs:: create_dir_all ( root. join ( "shop-web/src/main/java/com/example" ) ) . unwrap ( ) ;
3652+ fs:: create_dir_all ( root. join ( "shop-api" ) ) . unwrap ( ) ;
3653+ fs:: write (
3654+ root. join ( "pom.xml" ) ,
3655+ r#"<project><artifactId>shop</artifactId><packaging>pom</packaging><modules><module>shop-web</module><module>shop-api</module></modules></project>"# ,
3656+ )
3657+ . unwrap ( ) ;
3658+ fs:: write (
3659+ root. join ( "shop-web/pom.xml" ) ,
3660+ r#"<project><artifactId>shop-web</artifactId><build><plugins><plugin><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>"# ,
3661+ )
3662+ . unwrap ( ) ;
3663+ fs:: write (
3664+ root. join ( "shop-api/pom.xml" ) ,
3665+ r#"<project><artifactId>shop-api</artifactId></project>"# ,
3666+ )
3667+ . unwrap ( ) ;
3668+ fs:: write (
3669+ root. join ( source) ,
3670+ "package com.example; @SpringBootApplication class WebApp { public static void main(String[] args) {} }" ,
3671+ )
3672+ . unwrap ( ) ;
3673+
3674+ let generated = generate_with (
3675+ & root,
3676+ & [ source] ,
3677+ Some ( jdt_entrypoints ( & [ ( source, "com.example.WebApp" ) ] ) ) ,
3678+ ) ;
3679+ assert_eq ! ( generated[ "ok" ] , true , "{generated}" ) ;
3680+ let service = generated[ "data" ] [ "generated" ] [ "configurations" ]
3681+ . as_array ( )
3682+ . unwrap ( )
3683+ . iter ( )
3684+ . find ( |value| value[ "provider" ] == "spring-boot.maven" )
3685+ . cloned ( )
3686+ . unwrap_or_else ( || panic ! ( "missing Spring Boot service in {generated}" ) ) ;
3687+ assert_eq ! ( service[ "cwd" ] , "." ) ;
3688+ assert_eq ! ( service[ "extensions" ] [ "maven" ] [ "module" ] , "shop-web" ) ;
3689+ write_generated_run_document ( & root, & generated[ "data" ] [ "generated" ] ) ;
3690+
3691+ // A user-authored Maven Module entry has no detected reactor; its module was
3692+ // validated against the project root when it was created.
3693+ let created = run_config_command (
3694+ "runConfig.createUserConfiguration" ,
3695+ serde_json:: json!( {
3696+ "root" : root, "scope" : "project" , "name" : "API" , "type" : "mavenModule" ,
3697+ "module" : "shop-api"
3698+ } ) ,
3699+ ) ;
3700+ assert_eq ! ( created[ "ok" ] , true , "{created}" ) ;
3701+ fs:: write (
3702+ root. join ( ".lithe/run/configurations.json" ) ,
3703+ created[ "data" ] [ "document" ] . as_str ( ) . unwrap ( ) ,
3704+ )
3705+ . unwrap ( ) ;
3706+ let user_module_id = created[ "data" ] [ "id" ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
3707+
3708+ for ( scope, id, directory) in [
3709+ ( "local" , service[ "id" ] . as_str ( ) . unwrap ( ) , "shop-web" ) ,
3710+ ( "project" , user_module_id. as_str ( ) , "shop-api" ) ,
3711+ ] {
3712+ let updated = run_config_command (
3713+ "runConfig.updateOptions" ,
3714+ serde_json:: json!( {
3715+ "root" : root, "scope" : scope, "configurationId" : id,
3716+ "workingDirectory" : directory
3717+ } ) ,
3718+ ) ;
3719+ assert_eq ! ( updated[ "ok" ] , true , "{updated}" ) ;
3720+ let file = if scope == "local" {
3721+ "local.json"
3722+ } else {
3723+ "configurations.json"
3724+ } ;
3725+ fs:: write (
3726+ root. join ( ".lithe/run" ) . join ( file) ,
3727+ updated[ "data" ] [ "document" ] . as_str ( ) . unwrap ( ) ,
3728+ )
3729+ . unwrap ( ) ;
3730+ }
3731+
3732+ let resolved = run_config_command ( "runConfig.resolve" , serde_json:: json!( { "root" : root } ) ) ;
3733+ assert_eq ! ( resolved[ "ok" ] , true , "{resolved}" ) ;
3734+ let configurations = resolved[ "data" ] [ "configurations" ] . as_array ( ) . unwrap ( ) ;
3735+ for ( id, directory) in [
3736+ ( service[ "id" ] . as_str ( ) . unwrap ( ) , "shop-web" ) ,
3737+ ( user_module_id. as_str ( ) , "shop-api" ) ,
3738+ ] {
3739+ let configuration = configurations
3740+ . iter ( )
3741+ . find ( |value| value[ "id" ] == id)
3742+ . unwrap_or_else ( || panic ! ( "{id} was dropped: {resolved}" ) ) ;
3743+ assert_eq ! ( configuration[ "cwd" ] , directory) ;
3744+ assert ! (
3745+ !resolved[ "data" ] [ "diagnostics" ]
3746+ . as_array( )
3747+ . unwrap( )
3748+ . iter( )
3749+ . any( |value| value[ "id" ] == id) ,
3750+ "{resolved}"
3751+ ) ;
3752+ }
3753+ let service_configuration = configurations
3754+ . iter ( )
3755+ . find ( |value| value[ "id" ] == service[ "id" ] )
3756+ . unwrap ( ) ;
3757+ assert_eq ! (
3758+ service_configuration[ "extensions" ] [ "maven" ] [ "reactorPath" ] ,
3759+ "."
3760+ ) ;
3761+
3762+ let plan = run_config_command (
3763+ "runConfig.createLaunchPlan" ,
3764+ serde_json:: json!( {
3765+ "root" : root,
3766+ "configurationId" : service[ "id" ] ,
3767+ "javaLaunch" : {
3768+ "mainClass" : "com.example.WebApp" ,
3769+ "classPaths" : [ "/workspace/shop-web/target/classes" ] ,
3770+ "modulePaths" : [ ]
3771+ } ,
3772+ "mavenContext" : { "version" : 1 , "reactorPath" : "." , "profiles" : [ ] , "skipTests" : false }
3773+ } ) ,
3774+ ) ;
3775+ assert_eq ! ( plan[ "ok" ] , true , "{plan}" ) ;
3776+ assert_eq ! ( plan[ "data" ] [ "workingDirectory" ] , "shop-web" ) ;
3777+ assert_eq ! (
3778+ plan[ "data" ] [ "arguments" ] ,
3779+ serde_json:: json!( [ "com.example.WebApp" ] )
3780+ ) ;
3781+ }
3782+
3783+ /// Issue #861: resolution hides a configuration whose `cwd` is missing, so a
3784+ /// value that cannot name a project directory is rejected when saved, where
3785+ /// the editor shows the error, instead of making the entry vanish. Editor
3786+ /// variables are not expanded even when the directory they point at exists.
3787+ #[ test]
3788+ fn working_directory_override_must_name_an_existing_project_directory ( ) {
3789+ let root = temporary_root ( "run-config-invalid-cwd" ) ;
3790+ // The guard also removes the workspace when an assertion fails.
3791+ struct Cleanup ( PathBuf ) ;
3792+ impl Drop for Cleanup {
3793+ fn drop ( & mut self ) {
3794+ let _ = fs:: remove_dir_all ( & self . 0 ) ;
3795+ }
3796+ }
3797+ let _cleanup = Cleanup ( root. clone ( ) ) ;
3798+ fs:: create_dir_all ( root. join ( ".lithe/run" ) ) . unwrap ( ) ;
3799+ fs:: create_dir_all ( root. join ( "backend" ) ) . unwrap ( ) ;
3800+ fs:: write (
3801+ root. join ( ".lithe/run/generated.json" ) ,
3802+ r#"{"version":2,"configurations":[{
3803+ "id":"python:api","name":"API","provider":"python.script",
3804+ "command":"python3","args":["app.py"],"cwd":".","toolchains":{}
3805+ }]}"# ,
3806+ )
3807+ . unwrap ( ) ;
3808+
3809+ for ( directory, message) in [
3810+ (
3811+ "${workspaceFolder}/backend" ,
3812+ "Working directory variables are not supported; use a path relative to the project" ,
3813+ ) ,
3814+ (
3815+ "$PROJECT_DIR$/backend" ,
3816+ "Working directory variables are not supported; use a path relative to the project" ,
3817+ ) ,
3818+ ( "frontend" , "Project configuration directory does not exist" ) ,
3819+ (
3820+ "../outside" ,
3821+ "Project configuration paths must stay inside the project" ,
3822+ ) ,
3823+ ] {
3824+ for scope in [ "local" , "project" ] {
3825+ let updated = run_config_command (
3826+ "runConfig.updateOptions" ,
3827+ serde_json:: json!( {
3828+ "root" : root, "scope" : scope, "configurationId" : "python:api" ,
3829+ "workingDirectory" : directory
3830+ } ) ,
3831+ ) ;
3832+ assert_eq ! ( updated[ "ok" ] , false , "{directory}: {updated}" ) ;
3833+ assert_eq ! ( updated[ "error" ] [ "code" ] , "invalid_request" ) ;
3834+ assert_eq ! ( updated[ "error" ] [ "message" ] , message, "{directory}" ) ;
3835+ }
3836+ }
3837+
3838+ let absolute = root. join ( "backend" ) . canonicalize ( ) . unwrap ( ) ;
3839+ let updated = run_config_command (
3840+ "runConfig.updateOptions" ,
3841+ serde_json:: json!( {
3842+ "root" : root, "scope" : "local" , "configurationId" : "python:api" ,
3843+ "workingDirectory" : absolute
3844+ } ) ,
3845+ ) ;
3846+ assert_eq ! ( updated[ "ok" ] , true , "{updated}" ) ;
3847+ let document: Value =
3848+ serde_json:: from_str ( updated[ "data" ] [ "document" ] . as_str ( ) . unwrap ( ) ) . unwrap ( ) ;
3849+ assert_eq ! ( document[ "configurations" ] [ 0 ] [ "cwd" ] , "backend" ) ;
3850+ }
0 commit comments