organize and clean up

This commit is contained in:
bQUARKz 2026-02-23 20:47:42 +00:00
parent e8afc8ea0d
commit f7101fec4d
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
45 changed files with 410 additions and 747 deletions

View File

@ -22,7 +22,7 @@ public class DepsPipelineStage implements PipelineStage {
return ReadOnlyCollection.wrap(List.of(BuildingIssue
.builder()
.error(true)
.message("[DEPS]: root directory no found: " + ctx.getConfig().getRootProjectPath())
.message("[DEPS]: rootProjectId directory no found: " + ctx.getConfig().getRootProjectPath())
.build()));
}
final var cfg = new DependencyConfig(false, rootCanonPath);

View File

@ -0,0 +1,44 @@
package p.studio.compiler.dtos;
import com.fasterxml.jackson.annotation.*;
import lombok.Getter;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public record PrometeuManifestDTO(
String name,
String version,
String language,
List<DependencyDeclaration> dependencies) {
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
@JsonSubTypes.Type(value = DependencyDeclaration.Local.class),
@JsonSubTypes.Type(value = DependencyDeclaration.Git.class)
})
public interface DependencyDeclaration {
@Getter
class Local implements DependencyDeclaration {
private final String path;
@JsonCreator
public Local(@JsonProperty("path") final String path) {
this.path = path;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
class Git implements DependencyDeclaration {
private final String url;
private final String rev;
@JsonCreator
public Git(@JsonProperty("url") final String url, @JsonProperty("rev") final String rev) {
this.url = url;
this.rev = rev;
}
}
}
}

View File

@ -10,4 +10,5 @@ public class FrontendSpec {
private final String languageId;
private final ReadOnlySet<String> allowedExtensions;
private final ReadOnlySet<String> sourceRoots;
private final boolean caseSensitive;
}

View File

@ -1,9 +1,9 @@
package p.studio.compiler.source.identifiers;
public class FileId extends AbstractSourceIdentifier {
public static final FileId NONE = new FileId(-1L);
public class FileId extends SourceIdentifier {
public static final FileId NONE = new FileId(-1);
public FileId(long id) {
public FileId(int id) {
super(id);
}

View File

@ -1,7 +1,7 @@
package p.studio.compiler.source.identifiers;
public class ModuleId extends AbstractSourceIdentifier {
public ModuleId(long id) {
public class ModuleId extends SourceIdentifier {
public ModuleId(int id) {
super(id);
}
}

View File

@ -1,7 +1,7 @@
package p.studio.compiler.source.identifiers;
public class NameId extends AbstractSourceIdentifier {
public NameId(long id) {
public class NameId extends SourceIdentifier {
public NameId(int id) {
super(id);
}
}

View File

@ -1,7 +1,7 @@
package p.studio.compiler.source.identifiers;
public class NodeId extends AbstractSourceIdentifier {
public NodeId(long id) {
public class NodeId extends SourceIdentifier {
public NodeId(int id) {
super(id);
}
}

View File

@ -1,11 +1,7 @@
package p.studio.compiler.source.identifiers;
public class ProjectId extends AbstractSourceIdentifier {
public class ProjectId extends SourceIdentifier {
public ProjectId(int id) {
super(Integer.toUnsignedLong(id));
}
public ProjectId(long id) {
super(id);
}
}

View File

@ -3,18 +3,24 @@ package p.studio.compiler.source.identifiers;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import java.util.Optional;
import java.util.function.Supplier;
@Getter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
abstract class AbstractSourceIdentifier {
public abstract class SourceIdentifier {
@EqualsAndHashCode.Include
private final long id;
private final int id;
AbstractSourceIdentifier(long id) {
public SourceIdentifier(int id) {
this.id = id;
}
public int getIndex() {
return (int) id;
if (id < 0) {
throw new IllegalArgumentException("index should be valid (positive)");
}
return id;
}
@Override

View File

@ -1,6 +1,6 @@
package p.studio.compiler.source.identifiers;
public class SymbolId extends AbstractSourceIdentifier {
public class SymbolId extends SourceIdentifier {
public SymbolId(int id) {
super(id);
}

View File

@ -1,7 +1,7 @@
package p.studio.compiler.source.identifiers;
public class TypeId extends AbstractSourceIdentifier {
public TypeId(long id) {
public class TypeId extends SourceIdentifier {
public TypeId(int id) {
super(id);
}
}

View File

@ -0,0 +1,33 @@
package p.studio.compiler.source.tables;
import p.studio.compiler.source.identifiers.SourceIdentifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
public abstract class DenseTable<IDENTIFIER extends SourceIdentifier, VALUE> {
private final List<VALUE> values = new ArrayList<>();
private final Function<Integer, IDENTIFIER> identifierGenerator;
protected DenseTable(final Function<Integer, IDENTIFIER> identifierGenerator) {
this.identifierGenerator = Objects.requireNonNull(identifierGenerator);
}
public final int size() { return values.size(); }
public final VALUE get(IDENTIFIER id) {
return values.get(id.getIndex());
}
public IDENTIFIER register(VALUE value) {
final int idx = values.size();
values.add(value);
return identifierGenerator.apply(idx);
}
protected void clear() {
values.clear();
}
}

View File

@ -0,0 +1,11 @@
package p.studio.compiler.source.tables;
import p.studio.compiler.source.identifiers.FileId;
import java.nio.file.Path;
public class FileTable extends InternTable<FileId, Path> {
public FileTable() {
super(FileId::new);
}
}

View File

@ -0,0 +1,37 @@
package p.studio.compiler.source.tables;
import p.studio.compiler.source.identifiers.SourceIdentifier;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
public abstract class InternTable<IDENTIFIER extends SourceIdentifier, VALUE>
extends DenseTable<IDENTIFIER, VALUE> {
private final Map<VALUE, IDENTIFIER> identifierByValue = new HashMap<>();
public InternTable(Function<Integer, IDENTIFIER> identifierGenerator) {
super(identifierGenerator);
}
@Override
public IDENTIFIER register(final VALUE value) {
return identifierByValue.computeIfAbsent(value, super::register);
}
public IDENTIFIER get(final VALUE value) {
return identifierByValue.get(value);
}
@Override
public void clear() {
super.clear();
identifierByValue.clear();
}
public boolean containsKey(VALUE value) {
return identifierByValue.containsKey(value);
}
}

View File

@ -0,0 +1,11 @@
package p.studio.compiler.source.tables;
import p.studio.compiler.source.identifiers.ProjectId;
import java.nio.file.Path;
public class ProjectTable extends InternTable<ProjectId, Path> {
public ProjectTable() {
super(ProjectId::new);
}
}

View File

@ -0,0 +1,19 @@
package p.studio.compiler.utilities;
import com.fasterxml.jackson.databind.ObjectMapper;
import p.studio.compiler.dtos.PrometeuManifestDTO;
import java.io.IOException;
import java.nio.file.Path;
public final class PrometeuManifestMapper {
private static final ObjectMapper mapper = new ObjectMapper();
public static PrometeuManifestDTO read(Path manifestPath) {
try {
return mapper.readValue(manifestPath.toFile(), PrometeuManifestDTO.class);
} catch (IOException e) {
throw new IllegalStateException("failed to read manifest " + manifestPath, e);
}
}
}

View File

@ -1,8 +1,9 @@
package p.studio.compiler.models;
import p.studio.compiler.exceptions.BuildException;
import p.studio.compiler.messages.DependencyConfig;
import p.studio.compiler.source.identifiers.ProjectId;
import p.studio.compiler.exceptions.BuildException;
import p.studio.compiler.source.tables.ProjectTable;
import p.studio.utilities.structures.ReadOnlyList;
import java.nio.file.Path;
@ -15,16 +16,16 @@ public final class DependencyContext {
public Path mainProjectRootPathCanon;
// Phase 1 (Discover)
public final List<ProjectInfo> projectInfos = new ArrayList<>();
public final Map<Path, Long> projectIndexByDirectory = new HashMap<>();
public final Deque<Path> pending = new ArrayDeque<>();
public final ProjectInfoTable projectInfoTable = new ProjectInfoTable();
// Phase 2+
public final List<ProjectNode> projectNodes = new ArrayList<>();
public final Map<Path, ProjectId> projectIdByDirectoryRoot = new HashMap<>();
public final ProjectTable projectTable = new ProjectTable();
public final Map<String, Set<String>> projectNameAndVersions = new HashMap<>();
public final List<ProjectDescriptor> projectDescriptors = new ArrayList<>();
public final List<List<ProjectId>> dependenciesByProject = new ArrayList<>();
public ProjectId root;
public ProjectId rootProjectId;
public BuildStack stack;
private DependencyContext(DependencyConfig config) {
@ -40,32 +41,16 @@ public final class DependencyContext {
}
public ResolvedWorkspace toResolvedWorkspace() {
if (root == null) {
throw new BuildException("dependencies: internal error: root ProjectId not set");
if (rootProjectId == null) {
throw new BuildException("dependenciesByProjectId: internal error: rootProjectId ProjectId not set");
}
final var projectDescriptors = ReadOnlyList.wrap(projectNodes
.stream()
.map(n -> {
final var languageId = n.getLanguageId();
final var sourcePolicy = new SourcePolicy(ReadOnlyList.empty(), true); // TODO: source policy should come from a frontend registry anginst language id
return ProjectDescriptor
.builder()
.projectId(n.getProjectId())
.name(n.getName())
.version(n.getVersion())
.projectDir(n.getProjectRootPath())
.sourceRoots(n.getSourceRoots())
.languageId(languageId)
.sourcePolicy(sourcePolicy)
.build();
})
.toList());
final var edges = ReadOnlyList.wrap(this
final var projectDescriptors = ReadOnlyList.wrap(this.projectDescriptors);
final var dependenciesByProject = ReadOnlyList.wrap(this
.dependenciesByProject
.stream()
.map(ReadOnlyList::wrap)
.toList());
final var graph = new ResolvedGraph(root, projectDescriptors, edges);
return new ResolvedWorkspace(root, graph, stack);
final var workspaceGraph = new WorkspaceGraph(projectDescriptors, dependenciesByProject);
return new ResolvedWorkspace(rootProjectId, workspaceGraph, stack);
}
}

View File

@ -1,4 +0,0 @@
package p.studio.compiler.models;
public record LoadedFile(String uri, String text) {
}

View File

@ -1,17 +0,0 @@
package p.studio.compiler.models;
import java.util.List;
/**
* Sources already loaded by dependencies (IO happens in dependencies, not in pipeline).
*/
public record LoadedSources(
/**
* For each project in the stack, a list of files (uri + text).
*/
List<ProjectSources> perProject
) {
public LoadedSources {
perProject = List.copyOf(perProject);
}
}

View File

@ -3,18 +3,19 @@ package p.studio.compiler.models;
import lombok.Builder;
import lombok.Getter;
import p.studio.compiler.source.identifiers.ProjectId;
import p.studio.compiler.workspaces.DependencyReference;
import p.studio.utilities.structures.ReadOnlyList;
import java.nio.file.Path;
@Builder
@Getter
public class ProjectDescriptor {
public final class ProjectDescriptor {
private final ProjectId projectId;
private final Path projectRootPath;
private final String name;
private final String version;
private final Path projectDir;
private final ReadOnlyList<Path> sourceRoots;
private final String languageId;
private final SourcePolicy sourcePolicy;
private final ReadOnlyList<DependencyReference> dependencies;
private final FrontendSpec frontendSpec;
}

View File

@ -0,0 +1,9 @@
package p.studio.compiler.models;
import p.studio.compiler.source.identifiers.SourceIdentifier;
public class ProjectInfoId extends SourceIdentifier {
public ProjectInfoId(int id) {
super(id);
}
}

View File

@ -0,0 +1,9 @@
package p.studio.compiler.models;
import p.studio.compiler.source.tables.DenseTable;
public class ProjectInfoTable extends DenseTable<ProjectInfoId, ProjectInfo> {
protected ProjectInfoTable() {
super(ProjectInfoId::new);
}
}

View File

@ -1,22 +0,0 @@
package p.studio.compiler.models;
import lombok.Builder;
import lombok.Getter;
import p.studio.compiler.source.identifiers.ProjectId;
import p.studio.compiler.workspaces.DependencyReference;
import p.studio.utilities.structures.ReadOnlyList;
import java.nio.file.Path;
@Builder
@Getter
public final class ProjectNode {
private final FrontendSpec frontendSpec;
private final ProjectId projectId;
private final Path projectRootPath;
private final String name;
private final String version;
private final ReadOnlyList<Path> sourceRoots;
private final String languageId;
private final ReadOnlyList<DependencyReference> dependencies;
}

View File

@ -1,9 +0,0 @@
package p.studio.compiler.models;
import p.studio.compiler.source.identifiers.ProjectId;
import p.studio.utilities.structures.ReadOnlyList;
public record ProjectSources(
ProjectId projectId,
ReadOnlyList<LoadedFile> files) {
}

View File

@ -1,47 +0,0 @@
package p.studio.compiler.models;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import java.util.List;
import java.util.Optional;
public record PrometeuLock(
long schema,
List<LockMapping> mappings) {
public PrometeuLock {
mappings = mappings != null ? List.copyOf(mappings) : List.of();
}
public static PrometeuLock blank() {
return new PrometeuLock(0, List.of());
}
public Optional<String> lookupGitLocalDir(
final String url,
final String rev) {
return mappings
.stream()
.filter(m -> m instanceof LockMapping.Git g && g.url().equals(url) && g.rev().equals(rev))
.map(m -> ((LockMapping.Git) m).localDir())
.findFirst();
}
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "kind"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = LockMapping.Git.class, name = "git")
})
public interface LockMapping {
record Git(
String url,
String rev,
@JsonProperty("local-dir") String localDir
) implements LockMapping {}
}
}

View File

@ -1,88 +1,11 @@
package p.studio.compiler.models;
import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import p.studio.compiler.FrontendRegistryService;
import p.studio.compiler.exceptions.BuildException;
import p.studio.compiler.workspaces.DependencyReference;
import p.studio.utilities.structures.ReadOnlyList;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public record PrometeuManifest(
String name,
String version,
String language,
ReadOnlyList<DependencyDeclaration> dependencies) {
public static PrometeuManifest extract(final Path path, final ObjectMapper mapper) {
try {
final var root = mapper.readTree(path.toFile());
final var name = text(root, "name");
final var version = text(root, "version");
final var language = Optional
.ofNullable(root.get("language"))
.map(JsonNode::asText)
.orElseGet(() -> FrontendRegistryService.getDefaultFrontendSpec().getLanguageId());
final List<PrometeuManifest.DependencyDeclaration> dependencies = new ArrayList<>();
final var dependencyNodes = root.get("dependencies");
if (dependencyNodes != null && dependencyNodes.isArray()) {
for (final var d : dependencyNodes) {
if (d.has("path")) {
dependencies.add(new PrometeuManifest.DependencyDeclaration.Local(d.get("path").asText()));
} else if (d.has("url")) {
final var url = d.get("url").asText();
final var rev = d.has("rev") ? d.get("rev").asText(null) : null;
dependencies.add(new PrometeuManifest.DependencyDeclaration.Git(url, rev));
}
}
}
return new PrometeuManifest(name, version, language, ReadOnlyList.wrap(dependencies));
} catch (IOException e) {
throw new BuildException("dependencies: failed to read or parse prometeu manifest " + path, e);
}
}
private static String text(final JsonNode root, final String field) {
JsonNode n = root.get(field);
return n != null ? n.asText() : null;
}
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
@JsonSubTypes.Type(value = DependencyDeclaration.Local.class),
@JsonSubTypes.Type(value = DependencyDeclaration.Git.class)
})
public interface DependencyDeclaration {
@Getter
class Local implements DependencyDeclaration {
private final String path;
@JsonCreator
public Local(@JsonProperty("path") final String path) {
this.path = path;
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
class Git implements DependencyDeclaration {
private final String git;
private final String rev;
@JsonCreator
public Git(@JsonProperty("url") final String git, @JsonProperty("rev") final String rev) {
this.git = git;
this.rev = rev;
}
}
}
ReadOnlyList<DependencyReference> dependencies) {
}

View File

@ -1,13 +0,0 @@
package p.studio.compiler.models;
import p.studio.compiler.source.identifiers.ProjectId;
import p.studio.utilities.structures.ReadOnlyList;
public record ResolvedGraph(
ProjectId root,
ReadOnlyList<ProjectDescriptor> projects,
ReadOnlyList<ReadOnlyList<ProjectId>> edges) {
public ProjectDescriptor project(ProjectId id) {
return projects.get(id.getIndex());
}
}

View File

@ -4,6 +4,6 @@ import p.studio.compiler.source.identifiers.ProjectId;
public record ResolvedWorkspace(
ProjectId projectId,
ResolvedGraph graph,
WorkspaceGraph graph,
BuildStack stack) {
}

View File

@ -1,6 +0,0 @@
package p.studio.compiler.models;
import p.studio.utilities.structures.ReadOnlyList;
public record SourcePolicy(ReadOnlyList<String> extensions, boolean caseSensitive) {
}

View File

@ -0,0 +1,17 @@
package p.studio.compiler.models;
import p.studio.compiler.source.identifiers.ProjectId;
import p.studio.utilities.structures.ReadOnlyList;
public record WorkspaceGraph(
ReadOnlyList<ProjectDescriptor> projects,
ReadOnlyList<ReadOnlyList<ProjectId>> dependenciesByProjectId) {
public ProjectDescriptor getProjectDescriptor(final ProjectId projectId) {
return projects.get(projectId.getIndex());
}
public ReadOnlyList<ProjectId> getDependencies(final ProjectId projectId) {
return dependenciesByProjectId.get(projectId.getIndex());
}
}

View File

@ -1,31 +1,35 @@
package p.studio.compiler.workspaces.phases;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import p.studio.compiler.FrontendRegistryService;
import p.studio.compiler.dtos.PrometeuManifestDTO;
import p.studio.compiler.messages.BuildingIssue;
import p.studio.compiler.models.PrometeuManifest;
import p.studio.compiler.workspaces.DependencyPhase;
import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.models.ProjectInfo;
import p.studio.compiler.models.ProjectInfoId;
import p.studio.compiler.models.PrometeuManifest;
import p.studio.compiler.utilities.PrometeuManifestMapper;
import p.studio.compiler.workspaces.DependencyPhase;
import p.studio.compiler.workspaces.DependencyReference;
import p.studio.utilities.structures.ReadOnlyCollection;
import p.studio.utilities.structures.ReadOnlyList;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
public class DiscoverPhase implements DependencyPhase {
private final ObjectMapper mapper = new ObjectMapper();
@Override
public ReadOnlyCollection<BuildingIssue> run(final DependencyContext ctx) {
final Map<Path, ProjectInfoId> projectIndexByDirectory = new HashMap<>();
final List<BuildingIssue> issues = new ArrayList<>();
while (!ctx.pending.isEmpty()) {
final var rootPathCanon = ctx.pending.pollFirst();
if (ctx.projectIndexByDirectory.containsKey(rootPathCanon)) {
if (projectIndexByDirectory.containsKey(rootPathCanon)) {
continue;
}
@ -52,7 +56,14 @@ public class DiscoverPhase implements DependencyPhase {
continue;
}
final var manifest = PrometeuManifest.extract(manifestPathCanon, mapper);
final var prometeuManifestDTO = PrometeuManifestMapper.read(manifestPathCanon);
final var manifestMaybe = map(rootPathCanon, prometeuManifestDTO, issues);
if (manifestMaybe.isEmpty()) {
continue;
}
final var manifest = manifestMaybe.get();
final var frontendSpec = FrontendRegistryService.getFrontendSpec(manifest.language());
if (frontendSpec.isEmpty()) {
@ -65,7 +76,6 @@ public class DiscoverPhase implements DependencyPhase {
continue;
}
final long projectIndex = ctx.projectInfos.size();
final var projectInfo = ProjectInfo
.builder()
.rootDirectory(rootPathCanon)
@ -73,33 +83,92 @@ public class DiscoverPhase implements DependencyPhase {
.manifest(manifest)
.frontendSpec(frontendSpec.get())
.build();
ctx.projectInfos.add(projectInfo);
ctx.projectIndexByDirectory.put(rootPathCanon, projectIndex);
final var projectInfoId = ctx.projectInfoTable.register(projectInfo);
projectIndexByDirectory.put(rootPathCanon, projectInfoId);
for (final var dependencyDeclaration : manifest.dependencies()) {
if (dependencyDeclaration instanceof PrometeuManifest.DependencyDeclaration.Local local) {
final var dependencyPath = rootPathCanon.resolve(local.getPath());
try {
final var dependencyPathCanon = dependencyPath.toRealPath();
ctx.pending.add(dependencyPathCanon);
} catch (IOException e) {
final var issue = BuildingIssue
.builder()
.message("[DEPS]: dep canonPath does not exist: " + dependencyPath + " (" + manifest.name() + ")")
.exception(e)
.build();
issues.add(issue);
}
} else if (dependencyDeclaration instanceof PrometeuManifest.DependencyDeclaration.Git) {
final var issue = BuildingIssue
.builder()
.message("[DEPS]: url dependencies not yet supported " + manifest.name() + " (" + rootPathCanon + ")")
.build();
issues.add(issue);
}
}
manifest.dependencies().forEach(depRef -> ctx.pending.add(depRef.canonPath()));
ctx.projectNameAndVersions.computeIfAbsent(manifest.name(), ignore -> new HashSet<>()).add(manifest.version());
}
return ReadOnlyCollection.wrap(issues);
}
public static Optional<PrometeuManifest> map(
final Path rootPath,
final PrometeuManifestDTO dto,
final List<BuildingIssue> buildingIssues) {
if (StringUtils.isBlank(dto.name())) {
final var issue = BuildingIssue
.builder()
.error(true)
.message("[DEPS]: manifest missing 'name': " + rootPath)
.build();
buildingIssues.add(issue);
}
if (StringUtils.isBlank(dto.version())) {
final var issue = BuildingIssue
.builder()
.error(true)
.message("[DEPS]: manifest missing 'version': " + rootPath)
.build();
buildingIssues.add(issue);
}
final var language = StringUtils.isBlank(dto.language())
? FrontendRegistryService.getDefaultFrontendSpec().getLanguageId()
: dto.language();
final var dependencies = resolveDependencies(rootPath, dto.dependencies(), buildingIssues);
if (CollectionUtils.isNotEmpty(buildingIssues)) {
return Optional.empty();
}
return Optional.of(new PrometeuManifest(dto.name(), dto.version(), language, ReadOnlyList.wrap(dependencies)));
}
private static List<DependencyReference> resolveDependencies(
final Path rootProjectCanonPath,
final List<PrometeuManifestDTO.DependencyDeclaration> dependencies,
final List<BuildingIssue> buildingIssues) {
if (CollectionUtils.isEmpty(dependencies)) {
return List.of();
}
final var deps = new ArrayList<DependencyReference>(dependencies.size());
for (var dependency : dependencies) {
switch (dependency) {
case PrometeuManifestDTO.DependencyDeclaration.Local local -> {
try {
final Path dependencyPathCanon = rootProjectCanonPath.resolve(local.getPath()).toRealPath();
deps.add(new DependencyReference(dependencyPathCanon));
} catch (IOException e) {
final var issue = BuildingIssue
.builder()
.error(true)
.message("[DEPS]: failed to canonicalize dependency path: " + local.getPath() + " from (" + rootProjectCanonPath + ")")
.exception(e)
.build();
buildingIssues.add(issue);
}
}
case PrometeuManifestDTO.DependencyDeclaration.Git git -> {
final var issue = BuildingIssue
.builder()
.error(true)
.message("[DEPS]: git dependencies are not supported yet: " + git.getUrl() + " from (" + rootProjectCanonPath + ")")
.build();
buildingIssues.add(issue);
}
default -> {
}
}
}
return deps;
}
}

View File

@ -2,7 +2,7 @@ package p.studio.compiler.workspaces.phases;
import p.studio.compiler.messages.BuildingIssue;
import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.workspaces.*;
import p.studio.compiler.workspaces.DependencyPhase;
import p.studio.utilities.structures.ReadOnlyCollection;
import java.util.ArrayList;
@ -11,13 +11,13 @@ import java.util.Objects;
public final class LocalizePhase implements DependencyPhase {
@Override
public ReadOnlyCollection<BuildingIssue> run(final DependencyContext state) {
public ReadOnlyCollection<BuildingIssue> run(final DependencyContext ctx) {
final List<BuildingIssue> issues = new ArrayList<>();
for (int i = 0; i < state.projectNodes.size(); i++) {
final var fromProjectNode = state.projectNodes.get(i);
for (int i = 0; i < ctx.projectDescriptors.size(); i++) {
final var fromProjectNode = ctx.projectDescriptors.get(i);
for (final var dependencyReference : fromProjectNode.getDependencies()) {
final var dependencyReferenceCanonPath = dependencyReference.canonPath();
final var projectId = state.projectIdByDirectoryRoot.get(dependencyReferenceCanonPath);
final var projectId = ctx.projectTable.get(dependencyReferenceCanonPath);
if (Objects.isNull(projectId)) {
final var issue = BuildingIssue
.builder()
@ -27,7 +27,7 @@ public final class LocalizePhase implements DependencyPhase {
issues.add(issue);
continue;
}
state.dependenciesByProject.get(fromProjectNode.getProjectId().getIndex()).add(projectId);
ctx.dependenciesByProject.get(fromProjectNode.getProjectId().getIndex()).add(projectId);
}
}
return ReadOnlyCollection.wrap(issues);

View File

@ -1,49 +1,53 @@
package p.studio.compiler.workspaces.phases;
import p.studio.compiler.messages.BuildingIssue;
import p.studio.compiler.models.*;
import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.models.ProjectDescriptor;
import p.studio.compiler.models.ProjectInfo;
import p.studio.compiler.models.ProjectInfoId;
import p.studio.compiler.source.identifiers.ProjectId;
import p.studio.compiler.workspaces.*;
import p.studio.compiler.workspaces.DependencyPhase;
import p.studio.utilities.structures.ReadOnlyCollection;
import p.studio.utilities.structures.ReadOnlyList;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
public final class MaterializePhase implements DependencyPhase {
@Override
public ReadOnlyCollection<BuildingIssue> run(final DependencyContext ctx) {
// start all over again, we will re-populate the project nodes and edges based on the project infos
ctx.projectNodes.clear();
ctx.projectIdByDirectoryRoot.clear();
// to start all over again, we will re-populate the project nodes and dependenciesByProjectId based on the project infos
ctx.rootProjectId = null;
ctx.projectDescriptors.clear();
ctx.projectTable.clear();
ctx.dependenciesByProject.clear();
ctx.root = null;
final List<BuildingIssue> issues = new ArrayList<>();
for (int index = 0; index < ctx.projectInfos.size(); index++) {
final var projectId = new ProjectId(index);
final var projectInfo = ctx.projectInfos.get(index);
ctx.projectNodes.add(buildProjectNode(projectId, projectInfo, issues));
ctx.projectIdByDirectoryRoot.put(projectInfo.rootDirectory, projectId);
for (int index = 0; index < ctx.projectInfoTable.size(); index++) {
final var projectInfo = ctx.projectInfoTable.get(new ProjectInfoId(index));
final var projectId = ctx.projectTable.register(projectInfo.rootDirectory);
ctx.projectDescriptors.add(buildProjectDescriptor(projectId, projectInfo, issues));
ctx.dependenciesByProject.add(new ArrayList<>());
}
final var rootProjectId = ctx.projectIdByDirectoryRoot.get(ctx.mainProjectRootPathCanon);
final var rootProjectId = ctx.projectTable.get(ctx.mainProjectRootPathCanon);
if (rootProjectId == null) {
final var issue = BuildingIssue
.builder()
.message("[DEPS]: root project dir " + ctx.mainProjectRootPathCanon + " was not discovered/materialized")
.message("[DEPS]: rootProjectId project dir " + ctx.mainProjectRootPathCanon + " was not discovered/materialized")
.build();
issues.add(issue);
return ReadOnlyCollection.wrap(issues);
}
ctx.root = rootProjectId;
ctx.rootProjectId = rootProjectId;
return ReadOnlyCollection.wrap(issues);
}
private static ProjectNode buildProjectNode(
private static ProjectDescriptor buildProjectDescriptor(
final ProjectId projectId,
final ProjectInfo projectInfo,
final List<BuildingIssue> issues) {
@ -57,7 +61,8 @@ public final class MaterializePhase implements DependencyPhase {
} catch (IOException e) {
final var issue = BuildingIssue
.builder()
.message("[DEPS]: source root canonPath does not exist: " + sourceRootPath + " (from " + projectInfo.rootDirectory + ")")
.error(true)
.message("[DEPS]: source rootProjectId canonPath does not exist: " + sourceRootPath + " (from " + projectInfo.rootDirectory + ")")
.exception(e)
.build();
sourceRootIssues.add(issue);
@ -68,39 +73,15 @@ public final class MaterializePhase implements DependencyPhase {
issues.addAll(sourceRootIssues);
}
final List<DependencyReference> dependencyReferencies = new ArrayList<>();
for (PrometeuManifest.DependencyDeclaration d : projectInfo.manifest.dependencies()) {
if (d instanceof PrometeuManifest.DependencyDeclaration.Local l) {
final var dependencyPath = projectInfo.rootDirectory.resolve(l.getPath());
try {
final var dependencyPathCanon = dependencyPath.toRealPath();
dependencyReferencies.add(new DependencyReference(dependencyPathCanon));
} catch (IOException e) {
final var issue = BuildingIssue
.builder()
.message("[DEPS]: local dep canonPath does not exist: " + dependencyPath + " (from " + projectInfo.rootDirectory + ")")
.exception(e)
.build();
issues.add(issue);
}
} else if (d instanceof PrometeuManifest.DependencyDeclaration.Git g) {
final var issue = BuildingIssue
.builder()
.message("[DEPS]: url dependency '" + g.getGit() + "' requires an explicit 'rev' and lock mapping (not supported yet)")
.build();
issues.add(issue);
}
}
return ProjectNode
return ProjectDescriptor
.builder()
.projectId(projectId)
.projectRootPath(projectInfo.rootDirectory)
.name(projectInfo.manifest.name())
.version(projectInfo.manifest.version())
.languageId(projectInfo.manifest.language())
.frontendSpec(projectInfo.getFrontendSpec())
.sourceRoots(ReadOnlyList.wrap(sourceRoots))
.dependencies(ReadOnlyList.wrap(dependencyReferencies))
.dependencies(projectInfo.manifest.dependencies())
.build();
}
}

View File

@ -18,7 +18,7 @@ public final class SeedPhase implements DependencyPhase {
} catch (IOException e) {
final var issue = BuildingIssue
.builder()
.message("[DEPS]: failed to canonicalize root directory: " + ctx.config().cacheDir())
.message("[DEPS]: failed to canonicalize rootProjectId directory: " + ctx.config().cacheDir())
.build();
issues.add(issue);
}

View File

@ -2,6 +2,7 @@ package p.studio.compiler.workspaces.phases;
import p.studio.compiler.models.BuildStack;
import p.studio.compiler.messages.BuildingIssue;
import p.studio.compiler.models.ProjectInfoId;
import p.studio.compiler.source.identifiers.ProjectId;
import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.workspaces.DependencyPhase;
@ -20,8 +21,8 @@ public final class StackPhase implements DependencyPhase {
* Implements topological sort; detects dependency cycles; sets build stack
*/
@Override
public ReadOnlyCollection<BuildingIssue> run(DependencyContext ctx) {
final int n = ctx.projectNodes.size();
public ReadOnlyCollection<BuildingIssue> run(final DependencyContext ctx) {
final int n = ctx.projectDescriptors.size();
final int[] indeg = new int[n];
for (int from = 0; from < n; from++) {
for (final ProjectId to : ctx.dependenciesByProject.get(from)) {
@ -38,7 +39,7 @@ public final class StackPhase implements DependencyPhase {
// Performs topological sort using Kahn's algorithm
while (!q.isEmpty()) {
final int u = q.removeFirst();
travesalOrder.add(ctx.projectNodes.get(u).getProjectId());
travesalOrder.add(ctx.projectDescriptors.get(u).getProjectId());
for (final ProjectId v : ctx.dependenciesByProject.get(u)) {
if (--indeg[(int) v.getId()] == 0) {
q.addLast((int) v.getId());
@ -58,13 +59,13 @@ public final class StackPhase implements DependencyPhase {
if (scc.size() > 1) {
final var cycle = scc
.stream()
.map(i -> ctx.projectNodes.get(i).getProjectId())
.map(i -> ctx.projectDescriptors.get(i).getProjectId())
.toList();
cycles.add(cycle);
} else {
// size==1: cycle only if self-loop exists
final var u = scc.getFirst();
final var projectId = ctx.projectNodes.get(u).getProjectId();
final var projectId = ctx.projectDescriptors.get(u).getProjectId();
boolean selfLoop = false;
for (final var pu : ctx.dependenciesByProject.get(u)) {
if (pu.getIndex() == u) {
@ -80,10 +81,7 @@ public final class StackPhase implements DependencyPhase {
final var msg = "[DEPS]: cycle(s) detected:\n" + cycles.stream()
.map(projectIds -> " * " + projectIds.stream()
.map(pId -> ctx.projectInfos
.get(pId.getIndex())
.manifest
.name())
.map(pId -> ctx.projectInfoTable.get(new ProjectInfoId(pId.getIndex())).manifest.name())
.collect(joining(" -> "))
)
.collect(joining("\n"));

View File

@ -9,24 +9,40 @@ import java.util.List;
public final class ValidatePhase implements DependencyPhase {
@Override
public ReadOnlyCollection<BuildingIssue> run(DependencyContext state) {
if (state.root == null) {
public ReadOnlyCollection<BuildingIssue> run(DependencyContext ctx) {
if (ctx.rootProjectId == null) {
final var issue = BuildingIssue
.builder()
.message("[DEPS]: root ProjectId not set")
.error(true)
.message("[DEPS]: rootProjectId ProjectId not set")
.build();
return ReadOnlyCollection.wrap(List.of(issue));
}
// Ensure the edges list matches the number of nodes
if (state.dependenciesByProject.size() != state.projectNodes.size()) {
// Ensure the dependenciesByProjectId list matches the number of project descriptors
if (ctx.dependenciesByProject.size() != ctx.projectDescriptors.size()) {
final var issue = BuildingIssue
.builder()
.message("[DEPS]: internal error: edges list size mismatch")
.error(true)
.message("[DEPS]: internal error: dependenciesByProjectId list size mismatch")
.build();
return ReadOnlyCollection.wrap(List.of(issue));
}
// we should check and ensure uniformity to version across the same projects
for (final var entry : ctx.projectNameAndVersions.entrySet()) {
final var name = entry.getKey();
final var versions = entry.getValue();
if (versions.size() > 1) {
final var issue = BuildingIssue
.builder()
.error(true)
.message("[DEPS]: inconsistent version for project: " + name + " (" + versions + ")")
.build();
return ReadOnlyCollection.wrap(List.of(issue));
}
}
return ReadOnlyCollection.empty();
}
}

View File

@ -1,76 +0,0 @@
package p.studio.compiler.workspaces;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.studio.compiler.messages.DependencyConfig;
import p.studio.compiler.models.ResolvedWorkspace;
import p.studio.compiler.workspaces.phases.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class DependencyServiceTest {
@Test
void canonical_pipeline_e2e(@TempDir Path tempDir) throws IOException {
final Path root = tempDir;
final Path depA = root.resolve("depA");
Files.createDirectories(depA);
Files.createDirectories(root.resolve("src"));
Files.createDirectories(depA.resolve("src"));
writeManifest(root, "root-proj", "0.1.0", new String[]{"depA"});
writeManifest(depA, "dep-a", "0.1.0", new String[]{});
final var cfg = new DependencyConfig(false, root, List.of());
final var phases = List.of(
new SeedPhase(),
new DiscoverPhase(),
new MaterializePhase(),
new LocalizePhase(),
new ValidatePhase(),
new StackPhase(),
new PolicyPhase()
);
final var service = new DependencyService(phases);
final ResolvedWorkspace ws = service.run(cfg);
assertNotNull(ws);
assertNotNull(ws.graph());
assertEquals(2, ws.graph().projects().size());
// Obter ids por nome
var rootDesc = ws.graph().projects().stream().filter(p -> p.getName().equals("root-proj")).findFirst().orElseThrow();
var depDesc = ws.graph().projects().stream().filter(p -> p.getName().equals("dep-a")).findFirst().orElseThrow();
final var rootId = rootDesc.getProjectId();
final var depId = depDesc.getProjectId();
// Edges: root -> dep
assertTrue(ws.graph().edges().get(rootId.getIndex()).contains(depId));
assertTrue(ws.graph().edges().get(depId.getIndex()).isEmpty());
// Stack: de acordo com a implementação atual (arestas root->dep), root vem antes
assertEquals(rootId, ws.stack().projects().get(0));
assertEquals(depId, ws.stack().projects().get(1));
// workspace.root é o projeto root
assertEquals(rootId, ws.projectId());
}
private static void writeManifest(Path dir, String name, String version, String[] localDeps) throws IOException {
final StringBuilder deps = new StringBuilder();
deps.append("[");
for (int i = 0; i < localDeps.length; i++) {
if (i > 0) deps.append(",");
deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}");
}
deps.append("]");
final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + "\n}";
Files.writeString(dir.resolve("prometeu.json"), json);
}
}

View File

@ -1,55 +0,0 @@
package p.studio.compiler.workspaces;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.studio.compiler.messages.DependencyConfig;
import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.workspaces.phases.DiscoverPhase;
import p.studio.compiler.workspaces.phases.SeedPhase;
import p.studio.utilities.structures.ReadOnlyCollection;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
class DiscoverPhaseTest {
@Test
void discover_finds_root_and_local_dependency(@TempDir Path tempDir) throws IOException {
// Estrutura: root/ (prometeu.json) -> deps: ["depA/"] e cria depA/(prometeu.json)
final Path root = tempDir;
final Path depA = root.resolve("depA");
Files.createDirectories(depA);
writeManifest(root, "root-proj", "0.1.0", new String[]{"depA"}, null);
writeManifest(depA, "dep-a", "0.1.0", new String[]{}, null);
final var cfg = new DependencyConfig(false, root, java.util.List.of());
final var ctx = DependencyContext.seed(cfg);
assertTrue(ReadOnlyCollection.isEmpty(new SeedPhase().run(ctx)));
final var issues = new DiscoverPhase().run(ctx);
assertTrue(ReadOnlyCollection.isEmpty(issues), "Descoberta deve ocorrer sem issues");
assertEquals(2, ctx.projectInfos.size(), "Dois projetos devem ser descobertos");
assertTrue(ctx.projectIndexByDirectory.containsKey(root.toRealPath()), "Projeto raiz indexado");
assertTrue(ctx.projectIndexByDirectory.containsKey(depA.toRealPath()), "Projeto depA indexado");
assertEquals(0L, ctx.projectIndexByDirectory.get(root.toRealPath()));
assertEquals(1L, ctx.projectIndexByDirectory.get(depA.toRealPath()));
}
private static void writeManifest(Path dir, String name, String version, String[] localDeps, String language) throws IOException {
final StringBuilder deps = new StringBuilder();
deps.append("[");
for (int i = 0; i < localDeps.length; i++) {
if (i > 0) deps.append(",");
deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}");
}
deps.append("]");
final String langField = language == null ? "" : ",\n \"language\": \"" + language + "\"";
final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + langField + "\n}";
Files.writeString(dir.resolve("prometeu.json"), json);
}
}

View File

@ -1,55 +0,0 @@
package p.studio.compiler.workspaces;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.studio.compiler.messages.DependencyConfig;
import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.workspaces.phases.*;
import p.studio.utilities.structures.ReadOnlyCollection;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
class LocalizePhaseTest {
@Test
void localize_resolves_dependency_references(@TempDir Path tempDir) throws IOException {
final Path root = tempDir;
final Path depA = root.resolve("depA");
Files.createDirectories(depA);
Files.createDirectories(root.resolve("src"));
Files.createDirectories(depA.resolve("src"));
writeManifest(root, "root-proj", "0.1.0", new String[]{"depA"});
writeManifest(depA, "dep-a", "0.1.0", new String[]{});
final var cfg = new DependencyConfig(false, root, java.util.List.of());
final var ctx = DependencyContext.seed(cfg);
assertTrue(ReadOnlyCollection.isEmpty(new SeedPhase().run(ctx)));
assertTrue(ReadOnlyCollection.isEmpty(new DiscoverPhase().run(ctx)));
assertTrue(ReadOnlyCollection.isEmpty(new MaterializePhase().run(ctx)));
final var issues = new LocalizePhase().run(ctx);
assertTrue(ReadOnlyCollection.isEmpty(issues), "Localização deve ocorrer sem issues");
final var depId = ctx.projectIdByDirectoryRoot.get(depA.toRealPath());
assertNotNull(depId);
final var rootId = ctx.root;
assertTrue(ctx.dependenciesByProject.get(rootId.getIndex()).contains(depId), "Root deve depender de depA");
}
private static void writeManifest(Path dir, String name, String version, String[] localDeps) throws IOException {
final StringBuilder deps = new StringBuilder();
deps.append("[");
for (int i = 0; i < localDeps.length; i++) {
if (i > 0) deps.append(",");
deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}");
}
deps.append("]");
final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + "\n}";
Files.writeString(dir.resolve("prometeu.json"), json);
}
}

View File

@ -1,63 +0,0 @@
package p.studio.compiler.workspaces;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.studio.compiler.messages.DependencyConfig;
import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.workspaces.phases.DiscoverPhase;
import p.studio.compiler.workspaces.phases.MaterializePhase;
import p.studio.compiler.workspaces.phases.SeedPhase;
import p.studio.utilities.structures.ReadOnlyCollection;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
class MaterializePhaseTest {
@Test
void materialize_builds_nodes_and_sets_root(@TempDir Path tempDir) throws IOException {
final Path root = tempDir;
final Path depA = root.resolve("depA");
Files.createDirectories(depA);
Files.createDirectories(root.resolve("src"));
Files.createDirectories(depA.resolve("src"));
writeManifest(root, "root-proj", "0.1.0", new String[]{"depA"});
writeManifest(depA, "dep-a", "0.1.0", new String[]{});
final var cfg = new DependencyConfig(false, root, java.util.List.of());
final var ctx = DependencyContext.seed(cfg);
assertTrue(ReadOnlyCollection.isEmpty(new SeedPhase().run(ctx)));
assertTrue(ReadOnlyCollection.isEmpty(new DiscoverPhase().run(ctx)));
final var issues = new MaterializePhase().run(ctx);
assertTrue(ReadOnlyCollection.isEmpty(issues), "Materialização sem issues esperadas");
assertNotNull(ctx.root, "Root ProjectId deve ser definido");
assertEquals(2, ctx.projectNodes.size());
assertEquals(2, ctx.dependenciesByProject.size());
assertTrue(ctx.projectIdByDirectoryRoot.containsKey(root.toRealPath()));
assertTrue(ctx.projectIdByDirectoryRoot.containsKey(depA.toRealPath()));
final var rootNode = ctx.projectNodes.get(ctx.root.getIndex());
assertEquals("root-proj", rootNode.getName());
assertEquals("0.1.0", rootNode.getVersion());
assertEquals("pbs", rootNode.getLanguageId());
assertFalse(rootNode.getSourceRoots().isEmpty(), "Deve haver pelo menos um source root (src)");
}
private static void writeManifest(Path dir, String name, String version, String[] localDeps) throws IOException {
final StringBuilder deps = new StringBuilder();
deps.append("[");
for (int i = 0; i < localDeps.length; i++) {
if (i > 0) deps.append(",");
deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}");
}
deps.append("]");
final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + "\n}";
Files.writeString(dir.resolve("prometeu.json"), json);
}
}

View File

@ -1,28 +0,0 @@
package p.studio.compiler.workspaces;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.studio.compiler.messages.DependencyConfig;
import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.workspaces.phases.SeedPhase;
import p.studio.utilities.structures.ReadOnlyCollection;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
class SeedPhaseTest {
@Test
void seed_initializes_root_and_pending(@TempDir Path tempDir) throws Exception {
final var cfg = new DependencyConfig(false, tempDir, java.util.List.of());
final var ctx = DependencyContext.seed(cfg);
final var issues = new SeedPhase().run(ctx);
assertTrue(ReadOnlyCollection.isEmpty(issues), "Nenhum issue esperado na SeedPhase");
assertNotNull(ctx.mainProjectRootPathCanon, "Caminho canônico do projeto raiz deve ser inicializado");
assertEquals(1, ctx.pending.size(), "Uma entrada deve ser enfileirada para descoberta inicial");
assertEquals(tempDir.toRealPath(), ctx.mainProjectRootPathCanon);
}
}

View File

@ -1,58 +0,0 @@
package p.studio.compiler.workspaces;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.studio.compiler.messages.DependencyConfig;
import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.workspaces.phases.*;
import p.studio.utilities.structures.ReadOnlyCollection;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
class StackPhaseTest {
@Test
void stack_topologically_sorts_projects(@TempDir Path tempDir) throws IOException {
final Path root = tempDir;
final Path depA = root.resolve("depA");
Files.createDirectories(depA);
Files.createDirectories(root.resolve("src"));
Files.createDirectories(depA.resolve("src"));
writeManifest(root, "root-proj", "0.1.0", new String[]{"depA"});
writeManifest(depA, "dep-a", "0.1.0", new String[]{});
final var cfg = new DependencyConfig(false, root, java.util.List.of());
final var ctx = DependencyContext.seed(cfg);
assertTrue(ReadOnlyCollection.isEmpty(new SeedPhase().run(ctx)));
assertTrue(ReadOnlyCollection.isEmpty(new DiscoverPhase().run(ctx)));
assertTrue(ReadOnlyCollection.isEmpty(new MaterializePhase().run(ctx)));
assertTrue(ReadOnlyCollection.isEmpty(new LocalizePhase().run(ctx)));
final var issues = new StackPhase().run(ctx);
assertTrue(ReadOnlyCollection.isEmpty(issues), "Ordenação sem issues");
assertNotNull(ctx.stack);
assertEquals(2, ctx.stack.projects().size());
final var depId = ctx.projectIdByDirectoryRoot.get(depA.toRealPath());
final var rootId = ctx.root;
assertEquals(rootId, ctx.stack.projects().get(0), "Root (sem entradas) vem primeiro pelo algoritmo atual");
assertEquals(depId, ctx.stack.projects().get(1));
}
private static void writeManifest(Path dir, String name, String version, String[] localDeps) throws IOException {
final StringBuilder deps = new StringBuilder();
deps.append("[");
for (int i = 0; i < localDeps.length; i++) {
if (i > 0) deps.append(",");
deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}");
}
deps.append("]");
final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + "\n}";
Files.writeString(dir.resolve("prometeu.json"), json);
}
}

View File

@ -1,59 +0,0 @@
package p.studio.compiler.workspaces;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import p.studio.compiler.messages.DependencyConfig;
import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.workspaces.phases.*;
import p.studio.utilities.structures.ReadOnlyCollection;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
class ValidateAndPolicyPhasesTest {
@Test
void validate_checks_consistency_without_issues(@TempDir Path tempDir) throws IOException {
final Path root = tempDir;
final Path depA = root.resolve("depA");
Files.createDirectories(depA);
Files.createDirectories(root.resolve("src"));
Files.createDirectories(depA.resolve("src"));
writeManifest(root, "root-proj", "0.1.0", new String[]{"depA"});
writeManifest(depA, "dep-a", "0.1.0", new String[]{});
final var cfg = new DependencyConfig(false, root, java.util.List.of());
final var ctx = DependencyContext.seed(cfg);
assertTrue(ReadOnlyCollection.isEmpty(new SeedPhase().run(ctx)));
assertTrue(ReadOnlyCollection.isEmpty(new DiscoverPhase().run(ctx)));
assertTrue(ReadOnlyCollection.isEmpty(new MaterializePhase().run(ctx)));
assertTrue(ReadOnlyCollection.isEmpty(new LocalizePhase().run(ctx)));
final var issues = new ValidatePhase().run(ctx);
assertTrue(ReadOnlyCollection.isEmpty(issues), "Validação não deve reportar issues no cenário feliz");
}
@Test
void policy_is_noop(@TempDir Path tempDir) {
final var cfg = new DependencyConfig(false, tempDir, java.util.List.of());
final var ctx = DependencyContext.seed(cfg);
final var issues = new PolicyPhase().run(ctx);
assertTrue(ReadOnlyCollection.isEmpty(issues), "PolicyPhase atualmente é no-op");
}
private static void writeManifest(Path dir, String name, String version, String[] localDeps) throws IOException {
final StringBuilder deps = new StringBuilder();
deps.append("[");
for (int i = 0; i < localDeps.length; i++) {
if (i > 0) deps.append(",");
deps.append("{\"canonPath\":\"").append(localDeps[i]).append("\"}");
}
deps.append("]");
final String json = "{\n \"name\": \"" + name + "\",\n \"version\": \"" + version + "\",\n \"dependencies\": " + deps + "\n}";
Files.writeString(dir.resolve("prometeu.json"), json);
}
}

View File

@ -4,5 +4,6 @@ plugins {
dependencies {
api(libs.jackson.databind)
api(libs.apache.commons.lang3)
api(libs.apache.commons.collections)
}

View File

@ -5,6 +5,14 @@ import org.slf4j.Logger;
import java.util.function.Consumer;
public interface LogAggregator {
static LogAggregator empty() {
return with(s -> {});
}
static LogAggregator stdout() {
return with(System.out::println);
}
static LogAggregator with(final Consumer<String> consumer) {
return new LogAggregatorImpl(consumer);
}