diff --git a/.codacy.yml b/.codacy.yml index 8fa1ab2f131..4684fca159c 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -6,4 +6,5 @@ # package name contains capital letter and such names are conventional. --- exclude_paths: - - "eo-maven-plugin/src/test/java/org/eolang/maven/TranspileMojoIT.java" + - "eo-runtime/src/test/java/integration/JarIT.java" + - "eo-runtime/src/test/java/integration/EoMavenPlugin.java" diff --git a/eo-runtime/src/main/java/org/eolang/JavaPath.java b/eo-runtime/src/main/java/org/eolang/JavaPath.java index 3a51339f0d4..62ec614741b 100644 --- a/eo-runtime/src/main/java/org/eolang/JavaPath.java +++ b/eo-runtime/src/main/java/org/eolang/JavaPath.java @@ -4,6 +4,8 @@ */ package org.eolang; +import java.util.regex.Pattern; + /** * Java path. * @@ -18,6 +20,15 @@ * @since 0.29 */ final class JavaPath { + /** + * Phi pattern. + */ + private static final Pattern PHI = Pattern.compile("^Φ\\.?"); + + /** + * Dots pattern. + */ + private static final Pattern DOTS = Pattern.compile("(^|\\.)([^.]+)"); /** * Object name in eolang notation. @@ -34,17 +45,8 @@ final class JavaPath { @Override public String toString() { - if (!this.object.startsWith("Φ.")) { - throw new IllegalArgumentException( - String.format( - "Can't build path to .java file from FQN not started from '%s.'", - PhPackage.GLOBAL - ) - ); - } - return this.object - .substring(2) - .replaceAll("(^|\\.)([^.]+)", "$1EO$2") + return DOTS.matcher(JavaPath.PHI.matcher(this.object).replaceAll("")) + .replaceAll("$1EO$2") .replace("$", "$EO") .replace("-", "_"); } diff --git a/eo-runtime/src/main/java/org/eolang/PhPackage.java b/eo-runtime/src/main/java/org/eolang/PhPackage.java index e0a4f1c19b0..754353e1667 100644 --- a/eo-runtime/src/main/java/org/eolang/PhPackage.java +++ b/eo-runtime/src/main/java/org/eolang/PhPackage.java @@ -5,11 +5,7 @@ package org.eolang; -import java.io.File; import java.lang.reflect.InvocationTargetException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -69,18 +65,35 @@ public boolean hasRho() { @Override public Phi take(final String name) { - final String obj = String.join(".", this.pkg, name); - final String key = new JavaPath(obj).toString(); - return this.objects.computeIfAbsent( - key, - k -> { - final Phi initialized = this.loadPhi(key, obj); - if (!(initialized instanceof PhPackage)) { - initialized.put(Attr.RHO, this); - } - return initialized; + final String fqn = String.join(".", this.pkg, name); + final Phi taken; + if (name.equals(Attr.RHO)) { + if (this.objects.containsKey(Attr.RHO)) { + taken = this.objects.get(Attr.RHO); + } else { + throw new ExUnset( + String.format( + "The %s attribute is absent in package object '%s'", + Attr.RHO, this.pkg + ) + ); } - ).copy(); + } else if (this.objects.containsKey(fqn)) { + taken = this.objects.get(fqn).copy(); + } else if (name.contains(".")) { + final String[] parts = name.split("\\."); + Phi next = this.take(parts[0]); + for (int idx = 1; idx < parts.length; ++idx) { + next = next.take(parts[idx]); + } + taken = next; + } else { + final Phi loaded = this.loadPhi(fqn); + loaded.put(Attr.RHO, this); + this.put(fqn, loaded); + taken = this.take(name); + } + return taken; } @Override @@ -99,9 +112,7 @@ public void put(final int pos, final Phi object) { @Override public void put(final String name, final Phi object) { - throw new ExFailure( - "Can't #put(%s, %s) to package object \"%s\"", name, object, this.pkg - ); + this.objects.put(name, object); } @Override @@ -111,30 +122,30 @@ public byte[] delta() { /** * Load phi object by package name from ClassLoader. - * @param path Path to directory or .java file - * @param object Object FQN + * @param fqn FQN of the EO object * @return Phi */ - private Phi loadPhi(final String path, final String object) { - final Path pth = new File( - this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath() - ).toPath().resolve(path.replace(".", File.separator)); - final Phi phi; - if (Files.exists(pth) && Files.isDirectory(pth)) { - phi = new PhPackage(object); - } else { - final Path clazz = Paths.get(String.format("%s.class", pth)); - if (!Files.exists(clazz) || Files.isDirectory(clazz)) { - throw new ExFailure( - String.format("Couldn't find object '%s'", object) - ); - } + @SuppressWarnings("PMD.PreserveStackTrace") + private Phi loadPhi(final String fqn) { + final String target = new JavaPath(fqn).toString(); + Phi loaded; + try { + Class.forName(String.format("%s.package-info", target)); + loaded = new PhPackage(fqn); + } catch (final ClassNotFoundException pckg) { try { - phi = (Phi) Class.forName(path) + loaded = (Phi) Class.forName(target) .getConstructor() .newInstance(); - } catch (final ClassNotFoundException - | NoSuchMethodException + } catch (final ClassNotFoundException phi) { + throw new ExFailure( + String.format( + "Couldn't find object '%s' because there's no class or package '%s'", + fqn, target + ), + phi + ); + } catch (final NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException ex @@ -142,12 +153,12 @@ private Phi loadPhi(final String path, final String object) { throw new ExFailure( String.format( "Couldn't build Java object \"%s\" in EO package \"%s\"", - path, this.pkg + target, this.pkg ), ex ); } } - return phi; + return loaded; } } diff --git a/eo-runtime/src/test/java/integration/EoMavenPlugin.java b/eo-runtime/src/test/java/integration/EoMavenPlugin.java new file mode 100644 index 00000000000..add5cc4447a --- /dev/null +++ b/eo-runtime/src/test/java/integration/EoMavenPlugin.java @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Objectionary.com + * SPDX-License-Identifier: MIT + */ +package integration; + +import com.jcabi.manifests.Manifests; +import com.yegor256.farea.Farea; +import com.yegor256.farea.Plugin; +import java.io.IOException; + +/** + * The eo-maven-plugin appended to {@link Farea}. + * @since 0.54 + */ +final class EoMavenPlugin { + /** + * Farea. + */ + private final Farea farea; + + /** + * Ctor. + * @param farea Farea + */ + EoMavenPlugin(final Farea farea) { + this.farea = farea; + } + + /** + * Append eo-maven-plugin to farea build. + * @return Appended eo-maven-plugin + * @throws IOException If fails to append + */ + Plugin appended() throws IOException { + return this.farea + .build() + .plugins() + .append( + "org.eolang", + "eo-maven-plugin", + System.getProperty( + "eo.version", + Manifests.read("EO-Version") + ) + ); + } +} diff --git a/eo-runtime/src/test/java/integration/JarIT.java b/eo-runtime/src/test/java/integration/JarIT.java new file mode 100644 index 00000000000..fb8eeae0db2 --- /dev/null +++ b/eo-runtime/src/test/java/integration/JarIT.java @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2016-2025 Objectionary.com + * SPDX-License-Identifier: MIT + */ +package integration; + +import com.jcabi.manifests.Manifests; +import com.yegor256.Jaxec; +import com.yegor256.MayBeSlow; +import com.yegor256.Mktmp; +import com.yegor256.MktmpResolver; +import com.yegor256.WeAreOnline; +import com.yegor256.farea.Farea; +import com.yegor256.farea.RequisiteMatcher; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Integration test that runs simple EO program from packaged jar. + * @since 0.54 + */ +@SuppressWarnings({"JTCOP.RuleAllTestsHaveProductionClass", "JTCOP.RuleNotContainsTestWord"}) +@ExtendWith(MktmpResolver.class) +final class JarIT { + @Test + @ExtendWith(WeAreOnline.class) + @ExtendWith(MayBeSlow.class) + void runsProgramFromJar(final @Mktmp Path temp) throws IOException { + new Farea(temp).together( + f -> { + f.properties() + .set("project.build.sourceEncoding", StandardCharsets.UTF_8.name()) + .set("project.reporting.outputEncoding", StandardCharsets.UTF_8.name()); + f.files().file("src/main").save( + Paths.get(System.getProperty("user.dir")).resolve("src/main") + ); + f.files() + .file("src/main/eo/simple.eo") + .write( + "QQ.io.stdout \"Hello, world!\" > simple\n".getBytes(StandardCharsets.UTF_8) + ); + f.dependencies() + .append( + "org.eolang", + "eo-runtime", + System.getProperty( + "eo.version", + Manifests.read("EO-Version") + ) + ); + new EoMavenPlugin(f) + .appended() + .execution("compile") + .phase("generate-sources") + .goals("register", "compile", "transpile") + .configuration() + .set("failOnWarning", Boolean.FALSE.toString()) + .set("skipLinting", Boolean.TRUE.toString()); + f.exec("clean", "compile", "jar:jar"); + MatcherAssert.assertThat( + "Project must be successfully built and packaged into jar", + f.log(), + RequisiteMatcher.SUCCESS + ); + MatcherAssert.assertThat( + "Simple program must be successfully executed from jar", + new Jaxec( + "java", "-cp", "test-0.0.0.jar", + "-Dfile.encoding=UTF-8", "-Xss64M", "-Xms64M", + "org.eolang.Main", "simple" + ).withHome(temp.resolve("target")).exec().stdout(), + Matchers.allOf( + Matchers.containsString("Hello, world!"), + Matchers.containsString("[0x01] = true") + ) + ); + } + ); + } +} diff --git a/eo-runtime/src/test/java/integration/PhiUnphiIT.java b/eo-runtime/src/test/java/integration/PhiUnphiIT.java index 570e8460226..8d49ec4e72b 100644 --- a/eo-runtime/src/test/java/integration/PhiUnphiIT.java +++ b/eo-runtime/src/test/java/integration/PhiUnphiIT.java @@ -49,16 +49,8 @@ void runsTestsAfterPhiAndUnphi(final @Mktmp Path temp) throws IOException { "Saxon-HE", "12.4" ); - f.build() - .plugins() - .append( - "org.eolang", - "eo-maven-plugin", - System.getProperty( - "eo.version", - Manifests.read("EO-Version") - ) - ) + new EoMavenPlugin(f) + .appended() .execution("phi-unphi") .phase("process-sources") .goals( @@ -135,16 +127,8 @@ void runsTestsAfterPhiAndUnphi(final @Mktmp Path temp) throws IOException { .set("skipLinting", Boolean.TRUE.toString()) .set("ignoreRuntime", Boolean.TRUE.toString()) .set("placeBinariesThatHaveSources", Boolean.TRUE.toString()); - f.build() - .plugins() - .append( - "org.eolang", - "eo-maven-plugin", - System.getProperty( - "eo.version", - "1.0-SNAPSHOT" - ) - ) + new EoMavenPlugin(f) + .appended() .execution("tests") .phase("generate-test-sources") .goals( diff --git a/eo-runtime/src/test/java/integration/SnippetIT.java b/eo-runtime/src/test/java/integration/SnippetIT.java index 4d9078f3a9f..0c775b72cef 100644 --- a/eo-runtime/src/test/java/integration/SnippetIT.java +++ b/eo-runtime/src/test/java/integration/SnippetIT.java @@ -28,14 +28,6 @@ /** * Integration test for simple snippets. - * - *

This test will/may fail if you change something in {@code to-java.xsl} - * or some other place where Java sources are generated. This happens - * because this test relies on {@code eo-runtime.jar}, which it finds in Maven - * Central. Thus, when changes are made, it is recommended to disable this test. - * Then, when new {@code eo-runtime.jar} is - * released to Maven Central, you enable this test again.

- * * @since 0.1 */ @SuppressWarnings({"JTCOP.RuleAllTestsHaveProductionClass", "JTCOP.RuleNotContainsTestWord"}) @@ -84,16 +76,8 @@ void runsAllSnippets(final String yml, final @Mktmp Path temp) throws IOExceptio f.build() .properties() .set("directory", target); - f.build() - .plugins() - .append( - "org.eolang", - "eo-maven-plugin", - System.getProperty( - "eo.version", - Manifests.read("EO-Version") - ) - ) + new EoMavenPlugin(f) + .appended() .execution("compile") .phase("generate-sources") .goals("register", "compile", "transpile") diff --git a/eo-runtime/src/test/java/org/eolang/PhPackageTest.java b/eo-runtime/src/test/java/org/eolang/PhPackageTest.java index 5dc59d36879..68ce8dc6b2d 100644 --- a/eo-runtime/src/test/java/org/eolang/PhPackageTest.java +++ b/eo-runtime/src/test/java/org/eolang/PhPackageTest.java @@ -33,18 +33,41 @@ final class PhPackageTest { void copiesObject() { MatcherAssert.assertThat( AtCompositeTest.TO_ADD_MESSAGE, - Phi.Φ.take("org").take("eolang").take("seq"), + Phi.Φ.take("org.eolang.seq"), Matchers.not( Matchers.equalTo( - Phi.Φ.take("org").take("eolang").take("seq") + Phi.Φ.take("org.eolang.seq") ) ) ); } + @Test + void doesNotSetRhoToGlobalObject() { + Assertions.assertThrows( + ExUnset.class, + () -> Phi.Φ.take(Attr.RHO), + String.format( + "Global object '%s' must not have %s attribute", + PhPackage.GLOBAL, Attr.RHO + ) + ); + } + + @Test + void setsRhoToPackage() { + final Phi org = Phi.Φ.take("org"); + final Phi eolang = org.take("eolang"); + MatcherAssert.assertThat( + String.format("The %s attribute must be set to package object on dispatch", Attr.RHO), + eolang.take(Attr.RHO), + Matchers.equalTo(org) + ); + } + @Test void setsRhoToObject() { - final Phi eolang = Phi.Φ.take("org").take("eolang"); + final Phi eolang = Phi.Φ.take("org.eolang"); final Phi seq = eolang.take("seq"); MatcherAssert.assertThat( AtCompositeTest.TO_ADD_MESSAGE, @@ -57,9 +80,7 @@ void setsRhoToObject() { void findsLongClass() { MatcherAssert.assertThat( AtCompositeTest.TO_ADD_MESSAGE, - Phi.Φ.take("org") - .take("eolang") - .take("bytes$eq").copy(), + Phi.Φ.take("org.eolang.bytes$eq").copy(), Matchers.instanceOf(Phi.class) ); }