Skip to content

Commit 72078a4

Browse files
committed
feat(tests): add more tests for external dependencies
Signed-off-by: Markus <28785953+MarkusJx@users.noreply.github.com>
1 parent af4e8f3 commit 72078a4

6 files changed

Lines changed: 144 additions & 135 deletions

File tree

src/node/java.rs

Lines changed: 8 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@ use crate::node::java_interface_proxy::JavaInterfaceProxy;
1111
use crate::node::java_options::JavaOptions;
1212
use crate::node::napi_error::{MapToNapiError, NapiError};
1313
use crate::node::stdout_redirect::StdoutRedirect;
14-
use crate::node::util::parse_array_or_string;
14+
use crate::node::util::{list_files, parse_array_or_string};
1515
use futures::future;
1616
use lazy_static::lazy_static;
1717
use napi::{Env, JsFunction, JsObject, JsUnknown, ValueType};
1818
use std::collections::HashMap;
19-
use std::path::Path;
2019
use std::sync::{Arc, Mutex};
2120

2221
lazy_static! {
@@ -153,57 +152,13 @@ impl Java {
153152
Ok(())
154153
}
155154

156-
#[napi(ts_args_type = "dir: string | string[]")]
157-
pub fn append_classpath_dir(&mut self, dir: JsUnknown) -> napi::Result<()> {
158-
let mut paths = parse_array_or_string(dir)?
159-
.into_iter()
160-
.map(|p| p.trim().to_string())
161-
.map(|mut p| {
162-
if p.ends_with("/") || p.ends_with("\\") {
163-
p.pop();
164-
p + "/"
165-
} else {
166-
p + "/"
167-
}
168-
})
169-
.collect::<Vec<String>>();
170-
171-
let env = self.root_vm.attach_thread().map_napi_err()?;
172-
env.append_class_path(paths.clone()).map_napi_err()?;
173-
self.loaded_jars.append(&mut paths);
174-
175-
Ok(())
176-
}
177-
178-
#[napi(ts_args_type = "path: string | string[]")]
179-
pub fn append_any_to_classpath(&mut self, path: JsUnknown) -> napi::Result<()> {
180-
let mut paths = parse_array_or_string(path)?
181-
.into_iter()
182-
.map(|p| p.trim().to_string())
183-
.map(|mut p| -> napi::Result<String> {
184-
let path = Path::new(&p);
185-
if path.exists() {
186-
if path.is_dir() {
187-
if p.ends_with("/") || p.ends_with("\\") {
188-
p.pop();
189-
Ok(p + "/")
190-
} else {
191-
Ok(p)
192-
}
193-
} else if path.is_file() {
194-
Ok(p)
195-
} else {
196-
Err(NapiError::from(format!(
197-
"Path is neither a file nor a directory: {}",
198-
p
199-
))
200-
.into_napi())
201-
}
202-
} else {
203-
Err(NapiError::from(format!("Path does not exist: {}", p)).into_napi())
204-
}
205-
})
206-
.collect::<napi::Result<Vec<String>>>()?;
155+
#[napi]
156+
pub fn append_any_to_classpath(
157+
&mut self,
158+
#[napi(ts_arg_type = "string | string[]")] path: JsUnknown,
159+
recursive: Option<bool>,
160+
) -> napi::Result<()> {
161+
let mut paths = list_files(parse_array_or_string(path)?, recursive.unwrap_or(false))?;
207162

208163
let env = self.root_vm.attach_thread().map_napi_err()?;
209164
env.append_class_path(paths.clone()).map_napi_err()?;

src/node/util.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
use crate::node::napi_error::NapiError;
12
use napi::{JsString, JsUnknown};
3+
use std::collections::VecDeque;
4+
use std::path::Path;
25

36
/// Parse an JsUnknown that is either a JsString or a JsArray into a String
47
pub(crate) fn parse_array_or_string(value: JsUnknown) -> napi::Result<Vec<String>> {
@@ -16,3 +19,31 @@ pub(crate) fn parse_array_or_string(value: JsUnknown) -> napi::Result<Vec<String
1619

1720
Ok(res)
1821
}
22+
23+
pub(crate) fn list_files(dirs: Vec<String>, recursive: bool) -> napi::Result<Vec<String>> {
24+
let mut files = Vec::<String>::new();
25+
let mut q = VecDeque::from(dirs);
26+
27+
while let Some(dir) = q.pop_back() {
28+
let path = Path::new(&dir);
29+
if !path.exists() {
30+
return Err(NapiError::from(format!("Path '{}' does not exist", dir)).into_napi());
31+
} else if path.is_dir() {
32+
let inner = std::fs::read_dir(path)
33+
.map_err(|e| NapiError::to_napi_error(e.into()))?
34+
.filter_map(|e| e.ok())
35+
.filter_map(|e| e.path().to_str().map(|s| s.to_string()))
36+
.collect::<Vec<String>>();
37+
38+
if recursive {
39+
q.extend(inner);
40+
} else {
41+
files.extend(inner);
42+
}
43+
} else {
44+
files.push(dir);
45+
}
46+
}
47+
48+
Ok(files)
49+
}

test/ClassTest.test.ts

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
importClassAsync,
44
appendClasspath,
55
appendClasspathAny,
6-
appendClasspathDir,
76
} from '../.';
87
import { expect } from 'chai';
98
import { ClassTool, shouldIncreaseTimeout } from './testUtil';
@@ -15,7 +14,7 @@ let classTool: ClassTool | null = null;
1514
interface JarClassOpts {
1615
extraImports?: string[];
1716
extraCode?: string;
18-
extraCompilerOpts?: string[];
17+
classpath?: string[];
1918
}
2019

2120
function createJarWithBasicClass(
@@ -33,7 +32,7 @@ function createJarWithBasicClass(
3332
${opts?.extraCode ?? ''}
3433
}`,
3534
className,
36-
opts?.extraCompilerOpts ?? []
35+
opts?.classpath ?? []
3736
);
3837

3938
const fileName = `${pkgName.replaceAll('.', '/')}/${className}.class`;
@@ -123,19 +122,28 @@ describe('ClassTest', () => {
123122
extraImports: ['external.ExternalClass'],
124123
extraCode: `
125124
private final ExternalClass ext;
126-
125+
127126
public Class2() {
128127
this.ext = new ExternalClass();
129128
}
130-
129+
131130
public ExternalClass getExt() {
132131
return this.ext;
133132
}
134133
`,
135-
extraCompilerOpts: [
136-
'-classpath',
137-
path.join(classTool!.outDir, 'thirteenth.jar') + ':.',
138-
],
134+
classpath: [path.join(classTool!.outDir, 'twelfth.jar')],
135+
});
136+
createJarWithBasicClass('dyn.external', 'Class1', 'fourteenth.jar');
137+
createJarWithBasicClass('dyn.importing', 'Class2', 'fifteenth.jar', {
138+
extraImports: ['dyn.external.Class1'],
139+
extraCode: `
140+
public final Class<?> ext;
141+
142+
public Class2() throws ClassNotFoundException {
143+
this.ext = Class.forName("dyn.external.Class1");
144+
}
145+
`,
146+
classpath: [path.join(classTool!.outDir, 'fourteenth.jar')],
139147
});
140148
});
141149

@@ -265,6 +273,9 @@ describe('ClassTest', () => {
265273
}).timeout(timeout);
266274

267275
it('Classes from multiple jars', () => {
276+
expect(() => importClass('test.ClassWithPackage')).to.throw();
277+
expect(() => importClass('test.ClassWithPackageAndImport')).to.throw();
278+
268279
appendClasspath([
269280
path.join(classTool!.outDir, 'first.jar'),
270281
path.join(classTool!.outDir, 'second.jar'),
@@ -286,6 +297,9 @@ describe('ClassTest', () => {
286297
}).timeout(timeout);
287298

288299
it('Classes from multiple jars (async)', async () => {
300+
expect(() => importClass('async.Class1')).to.throw();
301+
expect(() => importClass('async.Class2')).to.throw();
302+
289303
appendClasspath([
290304
path.join(classTool!.outDir, 'third.jar'),
291305
path.join(classTool!.outDir, 'fourth.jar'),
@@ -307,7 +321,10 @@ describe('ClassTest', () => {
307321
}).timeout(timeout);
308322

309323
it('Classes from multiple jars with directory import', () => {
310-
appendClasspath(path.join(classTool!.outDir, 'dir') + '/');
324+
expect(() => importClass('dir.Class1')).to.throw();
325+
expect(() => importClass('dir.Class2')).to.throw();
326+
327+
appendClasspathAny(path.join(classTool!.outDir, 'dir'), true);
311328

312329
const Class1 = importClass('dir.Class1');
313330
const Class2 = importClass('dir.Class2');
@@ -323,6 +340,10 @@ describe('ClassTest', () => {
323340
}).timeout(timeout);
324341

325342
it('Classes from multiple jars with any import', () => {
343+
expect(() => importClass('any.Class1')).to.throw();
344+
expect(() => importClass('any.Class2')).to.throw();
345+
expect(() => importClass('other.Class1')).to.throw();
346+
326347
appendClasspathAny([
327348
path.join(classTool!.outDir, 'any'),
328349
path.join(classTool!.outDir, 'ninth.jar'),
@@ -347,7 +368,10 @@ describe('ClassTest', () => {
347368
}).timeout(timeout);
348369

349370
it('Classes from multiple jars with dir import', () => {
350-
appendClasspathDir(path.join(classTool!.outDir, 'dir1'));
371+
expect(() => importClass('dir1.Class1')).to.throw();
372+
expect(() => importClass('dir1.Class2')).to.throw();
373+
374+
appendClasspathAny(path.join(classTool!.outDir, 'dir1'));
351375

352376
const Class1 = importClass('dir1.Class1');
353377
const Class2 = importClass('dir1.Class2');
@@ -363,6 +387,9 @@ describe('ClassTest', () => {
363387
}).timeout(timeout);
364388

365389
it('Class with external dependency', () => {
390+
expect(() => importClass('importing.Class2')).to.throw();
391+
expect(() => importClass('external.ExternalClass')).to.throw();
392+
366393
appendClasspath([
367394
path.join(classTool!.outDir, 'twelfth.jar'),
368395
path.join(classTool!.outDir, 'thirteenth.jar'),
@@ -378,6 +405,28 @@ describe('ClassTest', () => {
378405
expect(ext).to.be.an('object');
379406
});
380407

408+
it('Class with external dependency loaded dynamically', () => {
409+
expect(() => importClass('dyn.importing.Class2')).to.throw();
410+
expect(() => importClass('dyn.external.Class1')).to.throw();
411+
412+
appendClasspath([
413+
path.join(classTool!.outDir, 'fourteenth.jar'),
414+
path.join(classTool!.outDir, 'fifteenth.jar'),
415+
]);
416+
417+
const Class2 = importClass('dyn.importing.Class2');
418+
expect(Class2).to.be.a('function');
419+
420+
const instance = new Class2();
421+
expect(instance).to.be.an('object');
422+
423+
const ext = instance.ext;
424+
expect(ext).to.be.an('object');
425+
426+
const instance2 = ext.newInstanceSync();
427+
expect(instance2).to.be.an('object');
428+
});
429+
381430
after(() => {
382431
classTool?.dispose();
383432
});

test/generateTestTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const classesToGenerate: string[] = [
3030
'java.net.URLClassLoader',
3131
'java.util.jar.JarEntry',
3232
'java.io.FileInputStream',
33+
'java.lang.System',
3334
];
3435

3536
function arrayEquals<T>(a: T[], b: T[]) {

test/testUtil.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import JarOutputStream from './javaDefinitions/java/util/jar/JarOutputStream';
1313
import Attributes$Name from './javaDefinitions/java/util/jar/Attributes$Name';
1414
import JarEntry from './javaDefinitions/java/util/jar/JarEntry';
1515
import FileInputStream from './javaDefinitions/java/io/FileInputStream';
16+
import System from './javaDefinitions/java/lang/System';
1617

1718
export const shouldIncreaseTimeout =
1819
isCi && (process.arch === 'arm64' || process.arch === 'arm');
@@ -63,34 +64,48 @@ export class ClassTool {
6364
public writeClass(
6465
code: string,
6566
className: string,
66-
extraOpts: string[] = []
67+
classpath: string[] = [],
68+
useGeneratedDir: boolean = false
6769
): void {
68-
const classFile = path.join(this.outDir, className + '.java');
70+
const outDir = useGeneratedDir
71+
? path.join(this.outDir, 'generated')
72+
: this.outDir;
73+
74+
if (useGeneratedDir && !fs.existsSync(outDir)) {
75+
fs.mkdirSync(outDir, {
76+
recursive: true,
77+
});
78+
}
79+
80+
const classFile = path.join(outDir, className + '.java');
6981
fs.writeFileSync(classFile, code, { encoding: 'utf8' });
7082

71-
console.log([
72-
...extraOpts,
73-
classFile,
74-
'-d',
75-
this.outDir,
76-
])
83+
const extraOpts: string[] = [];
84+
if (classpath.length > 0) {
85+
classpath.push(System.getPropertySync('java.class.path') || '.');
86+
extraOpts.push('-classpath', classpath.join(File.pathSeparator!));
87+
}
7788

7889
const compiler = ToolProvider.getSystemJavaCompilerSync();
7990
const res = compiler!.runSync(null, null, null, [
8091
...extraOpts,
8192
classFile,
8293
'-d',
83-
this.outDir,
94+
outDir,
8495
]);
8596

8697
if (res != 0) {
8798
throw new Error(`The compiler returned non-zero exit code: ${res}`);
8899
}
89100
}
90101

91-
public createClass(code: string, className: string): void {
92-
this.writeClass(code, className);
93-
const root = new File(this.outDir);
102+
public createClass(
103+
code: string,
104+
className: string,
105+
classpath: string[] = []
106+
): void {
107+
this.writeClass(code, className, classpath, true);
108+
const root = new File(path.join(this.outDir, 'generated'));
94109

95110
const prevClassLoader = getClassLoader() as ClassLoader;
96111
const classLoader = URLClassLoader.newInstanceSync(
@@ -106,6 +121,10 @@ export class ClassTool {
106121
}
107122

108123
public dispose(): void {
109-
fs.rmSync(this.outDir, { recursive: true });
124+
try {
125+
fs.rmSync(this.outDir, { recursive: true, force: true });
126+
} catch (e) {
127+
console.error(e);
128+
}
110129
}
111130
}

0 commit comments

Comments
 (0)