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 return ReadOnlyCollection.wrap(List.of(BuildingIssue
.builder() .builder()
.error(true) .error(true)
.message("[DEPS]: root directory no found: " + ctx.getConfig().getRootProjectPath()) .message("[DEPS]: rootProjectId directory no found: " + ctx.getConfig().getRootProjectPath())
.build())); .build()));
} }
final var cfg = new DependencyConfig(false, rootCanonPath); 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 String languageId;
private final ReadOnlySet<String> allowedExtensions; private final ReadOnlySet<String> allowedExtensions;
private final ReadOnlySet<String> sourceRoots; private final ReadOnlySet<String> sourceRoots;
private final boolean caseSensitive;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
package p.studio.compiler.source.identifiers; package p.studio.compiler.source.identifiers;
public class TypeId extends AbstractSourceIdentifier { public class TypeId extends SourceIdentifier {
public TypeId(long id) { public TypeId(int id) {
super(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; package p.studio.compiler.models;
import p.studio.compiler.exceptions.BuildException;
import p.studio.compiler.messages.DependencyConfig; import p.studio.compiler.messages.DependencyConfig;
import p.studio.compiler.source.identifiers.ProjectId; 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 p.studio.utilities.structures.ReadOnlyList;
import java.nio.file.Path; import java.nio.file.Path;
@ -15,16 +16,16 @@ public final class DependencyContext {
public Path mainProjectRootPathCanon; public Path mainProjectRootPathCanon;
// Phase 1 (Discover) // 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 Deque<Path> pending = new ArrayDeque<>();
public final ProjectInfoTable projectInfoTable = new ProjectInfoTable();
// Phase 2+ public final ProjectTable projectTable = new ProjectTable();
public final List<ProjectNode> projectNodes = new ArrayList<>();
public final Map<Path, ProjectId> projectIdByDirectoryRoot = new HashMap<>(); 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 final List<List<ProjectId>> dependenciesByProject = new ArrayList<>();
public ProjectId root; public ProjectId rootProjectId;
public BuildStack stack; public BuildStack stack;
private DependencyContext(DependencyConfig config) { private DependencyContext(DependencyConfig config) {
@ -40,32 +41,16 @@ public final class DependencyContext {
} }
public ResolvedWorkspace toResolvedWorkspace() { public ResolvedWorkspace toResolvedWorkspace() {
if (root == null) { if (rootProjectId == null) {
throw new BuildException("dependencies: internal error: root ProjectId not set"); throw new BuildException("dependenciesByProjectId: internal error: rootProjectId ProjectId not set");
} }
final var projectDescriptors = ReadOnlyList.wrap(projectNodes final var projectDescriptors = ReadOnlyList.wrap(this.projectDescriptors);
.stream() final var dependenciesByProject = ReadOnlyList.wrap(this
.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
.dependenciesByProject .dependenciesByProject
.stream() .stream()
.map(ReadOnlyList::wrap) .map(ReadOnlyList::wrap)
.toList()); .toList());
final var graph = new ResolvedGraph(root, projectDescriptors, edges); final var workspaceGraph = new WorkspaceGraph(projectDescriptors, dependenciesByProject);
return new ResolvedWorkspace(root, graph, stack); 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.Builder;
import lombok.Getter; import lombok.Getter;
import p.studio.compiler.source.identifiers.ProjectId; import p.studio.compiler.source.identifiers.ProjectId;
import p.studio.compiler.workspaces.DependencyReference;
import p.studio.utilities.structures.ReadOnlyList; import p.studio.utilities.structures.ReadOnlyList;
import java.nio.file.Path; import java.nio.file.Path;
@Builder @Builder
@Getter @Getter
public class ProjectDescriptor { public final class ProjectDescriptor {
private final ProjectId projectId; private final ProjectId projectId;
private final Path projectRootPath;
private final String name; private final String name;
private final String version; private final String version;
private final Path projectDir;
private final ReadOnlyList<Path> sourceRoots; private final ReadOnlyList<Path> sourceRoots;
private final String languageId; private final ReadOnlyList<DependencyReference> dependencies;
private final SourcePolicy sourcePolicy; 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; package p.studio.compiler.models;
import com.fasterxml.jackson.annotation.*; import p.studio.compiler.workspaces.DependencyReference;
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.utilities.structures.ReadOnlyList; 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( public record PrometeuManifest(
String name, String name,
String version, String version,
String language, String language,
ReadOnlyList<DependencyDeclaration> dependencies) { ReadOnlyList<DependencyReference> 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;
}
}
}
} }

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( public record ResolvedWorkspace(
ProjectId projectId, ProjectId projectId,
ResolvedGraph graph, WorkspaceGraph graph,
BuildStack stack) { 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; 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.FrontendRegistryService;
import p.studio.compiler.dtos.PrometeuManifestDTO;
import p.studio.compiler.messages.BuildingIssue; 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.DependencyContext;
import p.studio.compiler.models.ProjectInfo; 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.ReadOnlyCollection;
import p.studio.utilities.structures.ReadOnlyList;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.*;
import java.util.List;
public class DiscoverPhase implements DependencyPhase { public class DiscoverPhase implements DependencyPhase {
private final ObjectMapper mapper = new ObjectMapper();
@Override @Override
public ReadOnlyCollection<BuildingIssue> run(final DependencyContext ctx) { public ReadOnlyCollection<BuildingIssue> run(final DependencyContext ctx) {
final Map<Path, ProjectInfoId> projectIndexByDirectory = new HashMap<>();
final List<BuildingIssue> issues = new ArrayList<>(); final List<BuildingIssue> issues = new ArrayList<>();
while (!ctx.pending.isEmpty()) { while (!ctx.pending.isEmpty()) {
final var rootPathCanon = ctx.pending.pollFirst(); final var rootPathCanon = ctx.pending.pollFirst();
if (ctx.projectIndexByDirectory.containsKey(rootPathCanon)) { if (projectIndexByDirectory.containsKey(rootPathCanon)) {
continue; continue;
} }
@ -52,7 +56,14 @@ public class DiscoverPhase implements DependencyPhase {
continue; 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()); final var frontendSpec = FrontendRegistryService.getFrontendSpec(manifest.language());
if (frontendSpec.isEmpty()) { if (frontendSpec.isEmpty()) {
@ -65,7 +76,6 @@ public class DiscoverPhase implements DependencyPhase {
continue; continue;
} }
final long projectIndex = ctx.projectInfos.size();
final var projectInfo = ProjectInfo final var projectInfo = ProjectInfo
.builder() .builder()
.rootDirectory(rootPathCanon) .rootDirectory(rootPathCanon)
@ -73,33 +83,92 @@ public class DiscoverPhase implements DependencyPhase {
.manifest(manifest) .manifest(manifest)
.frontendSpec(frontendSpec.get()) .frontendSpec(frontendSpec.get())
.build(); .build();
ctx.projectInfos.add(projectInfo); final var projectInfoId = ctx.projectInfoTable.register(projectInfo);
ctx.projectIndexByDirectory.put(rootPathCanon, projectIndex); projectIndexByDirectory.put(rootPathCanon, projectInfoId);
for (final var dependencyDeclaration : manifest.dependencies()) { manifest.dependencies().forEach(depRef -> ctx.pending.add(depRef.canonPath()));
if (dependencyDeclaration instanceof PrometeuManifest.DependencyDeclaration.Local local) {
final var dependencyPath = rootPathCanon.resolve(local.getPath()); ctx.projectNameAndVersions.computeIfAbsent(manifest.name(), ignore -> new HashSet<>()).add(manifest.version());
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);
}
}
} }
return ReadOnlyCollection.wrap(issues); 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.messages.BuildingIssue;
import p.studio.compiler.models.DependencyContext; import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.workspaces.*; import p.studio.compiler.workspaces.DependencyPhase;
import p.studio.utilities.structures.ReadOnlyCollection; import p.studio.utilities.structures.ReadOnlyCollection;
import java.util.ArrayList; import java.util.ArrayList;
@ -11,13 +11,13 @@ import java.util.Objects;
public final class LocalizePhase implements DependencyPhase { public final class LocalizePhase implements DependencyPhase {
@Override @Override
public ReadOnlyCollection<BuildingIssue> run(final DependencyContext state) { public ReadOnlyCollection<BuildingIssue> run(final DependencyContext ctx) {
final List<BuildingIssue> issues = new ArrayList<>(); final List<BuildingIssue> issues = new ArrayList<>();
for (int i = 0; i < state.projectNodes.size(); i++) { for (int i = 0; i < ctx.projectDescriptors.size(); i++) {
final var fromProjectNode = state.projectNodes.get(i); final var fromProjectNode = ctx.projectDescriptors.get(i);
for (final var dependencyReference : fromProjectNode.getDependencies()) { for (final var dependencyReference : fromProjectNode.getDependencies()) {
final var dependencyReferenceCanonPath = dependencyReference.canonPath(); final var dependencyReferenceCanonPath = dependencyReference.canonPath();
final var projectId = state.projectIdByDirectoryRoot.get(dependencyReferenceCanonPath); final var projectId = ctx.projectTable.get(dependencyReferenceCanonPath);
if (Objects.isNull(projectId)) { if (Objects.isNull(projectId)) {
final var issue = BuildingIssue final var issue = BuildingIssue
.builder() .builder()
@ -27,7 +27,7 @@ public final class LocalizePhase implements DependencyPhase {
issues.add(issue); issues.add(issue);
continue; continue;
} }
state.dependenciesByProject.get(fromProjectNode.getProjectId().getIndex()).add(projectId); ctx.dependenciesByProject.get(fromProjectNode.getProjectId().getIndex()).add(projectId);
} }
} }
return ReadOnlyCollection.wrap(issues); return ReadOnlyCollection.wrap(issues);

View File

@ -1,49 +1,53 @@
package p.studio.compiler.workspaces.phases; package p.studio.compiler.workspaces.phases;
import p.studio.compiler.messages.BuildingIssue; 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.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.ReadOnlyCollection;
import p.studio.utilities.structures.ReadOnlyList; import p.studio.utilities.structures.ReadOnlyList;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.*; import java.util.ArrayList;
import java.util.List;
public final class MaterializePhase implements DependencyPhase { public final class MaterializePhase implements DependencyPhase {
@Override @Override
public ReadOnlyCollection<BuildingIssue> run(final DependencyContext ctx) { 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 // to start all over again, we will re-populate the project nodes and dependenciesByProjectId based on the project infos
ctx.projectNodes.clear(); ctx.rootProjectId = null;
ctx.projectIdByDirectoryRoot.clear(); ctx.projectDescriptors.clear();
ctx.projectTable.clear();
ctx.dependenciesByProject.clear(); ctx.dependenciesByProject.clear();
ctx.root = null;
final List<BuildingIssue> issues = new ArrayList<>(); final List<BuildingIssue> issues = new ArrayList<>();
for (int index = 0; index < ctx.projectInfos.size(); index++) { for (int index = 0; index < ctx.projectInfoTable.size(); index++) {
final var projectId = new ProjectId(index); final var projectInfo = ctx.projectInfoTable.get(new ProjectInfoId(index));
final var projectInfo = ctx.projectInfos.get(index); final var projectId = ctx.projectTable.register(projectInfo.rootDirectory);
ctx.projectNodes.add(buildProjectNode(projectId, projectInfo, issues)); ctx.projectDescriptors.add(buildProjectDescriptor(projectId, projectInfo, issues));
ctx.projectIdByDirectoryRoot.put(projectInfo.rootDirectory, projectId);
ctx.dependenciesByProject.add(new ArrayList<>()); ctx.dependenciesByProject.add(new ArrayList<>());
} }
final var rootProjectId = ctx.projectIdByDirectoryRoot.get(ctx.mainProjectRootPathCanon); final var rootProjectId = ctx.projectTable.get(ctx.mainProjectRootPathCanon);
if (rootProjectId == null) { if (rootProjectId == null) {
final var issue = BuildingIssue final var issue = BuildingIssue
.builder() .builder()
.message("[DEPS]: root project dir " + ctx.mainProjectRootPathCanon + " was not discovered/materialized") .message("[DEPS]: rootProjectId project dir " + ctx.mainProjectRootPathCanon + " was not discovered/materialized")
.build(); .build();
issues.add(issue); issues.add(issue);
return ReadOnlyCollection.wrap(issues); return ReadOnlyCollection.wrap(issues);
} }
ctx.root = rootProjectId;
ctx.rootProjectId = rootProjectId;
return ReadOnlyCollection.wrap(issues); return ReadOnlyCollection.wrap(issues);
} }
private static ProjectNode buildProjectNode( private static ProjectDescriptor buildProjectDescriptor(
final ProjectId projectId, final ProjectId projectId,
final ProjectInfo projectInfo, final ProjectInfo projectInfo,
final List<BuildingIssue> issues) { final List<BuildingIssue> issues) {
@ -57,7 +61,8 @@ public final class MaterializePhase implements DependencyPhase {
} catch (IOException e) { } catch (IOException e) {
final var issue = BuildingIssue final var issue = BuildingIssue
.builder() .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) .exception(e)
.build(); .build();
sourceRootIssues.add(issue); sourceRootIssues.add(issue);
@ -68,39 +73,15 @@ public final class MaterializePhase implements DependencyPhase {
issues.addAll(sourceRootIssues); issues.addAll(sourceRootIssues);
} }
final List<DependencyReference> dependencyReferencies = new ArrayList<>(); return ProjectDescriptor
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
.builder() .builder()
.projectId(projectId) .projectId(projectId)
.projectRootPath(projectInfo.rootDirectory) .projectRootPath(projectInfo.rootDirectory)
.name(projectInfo.manifest.name()) .name(projectInfo.manifest.name())
.version(projectInfo.manifest.version()) .version(projectInfo.manifest.version())
.languageId(projectInfo.manifest.language()) .frontendSpec(projectInfo.getFrontendSpec())
.sourceRoots(ReadOnlyList.wrap(sourceRoots)) .sourceRoots(ReadOnlyList.wrap(sourceRoots))
.dependencies(ReadOnlyList.wrap(dependencyReferencies)) .dependencies(projectInfo.manifest.dependencies())
.build(); .build();
} }
} }

View File

@ -18,7 +18,7 @@ public final class SeedPhase implements DependencyPhase {
} catch (IOException e) { } catch (IOException e) {
final var issue = BuildingIssue final var issue = BuildingIssue
.builder() .builder()
.message("[DEPS]: failed to canonicalize root directory: " + ctx.config().cacheDir()) .message("[DEPS]: failed to canonicalize rootProjectId directory: " + ctx.config().cacheDir())
.build(); .build();
issues.add(issue); 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.models.BuildStack;
import p.studio.compiler.messages.BuildingIssue; import p.studio.compiler.messages.BuildingIssue;
import p.studio.compiler.models.ProjectInfoId;
import p.studio.compiler.source.identifiers.ProjectId; import p.studio.compiler.source.identifiers.ProjectId;
import p.studio.compiler.models.DependencyContext; import p.studio.compiler.models.DependencyContext;
import p.studio.compiler.workspaces.DependencyPhase; 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 * Implements topological sort; detects dependency cycles; sets build stack
*/ */
@Override @Override
public ReadOnlyCollection<BuildingIssue> run(DependencyContext ctx) { public ReadOnlyCollection<BuildingIssue> run(final DependencyContext ctx) {
final int n = ctx.projectNodes.size(); final int n = ctx.projectDescriptors.size();
final int[] indeg = new int[n]; final int[] indeg = new int[n];
for (int from = 0; from < n; from++) { for (int from = 0; from < n; from++) {
for (final ProjectId to : ctx.dependenciesByProject.get(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 // Performs topological sort using Kahn's algorithm
while (!q.isEmpty()) { while (!q.isEmpty()) {
final int u = q.removeFirst(); 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)) { for (final ProjectId v : ctx.dependenciesByProject.get(u)) {
if (--indeg[(int) v.getId()] == 0) { if (--indeg[(int) v.getId()] == 0) {
q.addLast((int) v.getId()); q.addLast((int) v.getId());
@ -58,13 +59,13 @@ public final class StackPhase implements DependencyPhase {
if (scc.size() > 1) { if (scc.size() > 1) {
final var cycle = scc final var cycle = scc
.stream() .stream()
.map(i -> ctx.projectNodes.get(i).getProjectId()) .map(i -> ctx.projectDescriptors.get(i).getProjectId())
.toList(); .toList();
cycles.add(cycle); cycles.add(cycle);
} else { } else {
// size==1: cycle only if self-loop exists // size==1: cycle only if self-loop exists
final var u = scc.getFirst(); final var u = scc.getFirst();
final var projectId = ctx.projectNodes.get(u).getProjectId(); final var projectId = ctx.projectDescriptors.get(u).getProjectId();
boolean selfLoop = false; boolean selfLoop = false;
for (final var pu : ctx.dependenciesByProject.get(u)) { for (final var pu : ctx.dependenciesByProject.get(u)) {
if (pu.getIndex() == 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() final var msg = "[DEPS]: cycle(s) detected:\n" + cycles.stream()
.map(projectIds -> " * " + projectIds.stream() .map(projectIds -> " * " + projectIds.stream()
.map(pId -> ctx.projectInfos .map(pId -> ctx.projectInfoTable.get(new ProjectInfoId(pId.getIndex())).manifest.name())
.get(pId.getIndex())
.manifest
.name())
.collect(joining(" -> ")) .collect(joining(" -> "))
) )
.collect(joining("\n")); .collect(joining("\n"));

View File

@ -9,24 +9,40 @@ import java.util.List;
public final class ValidatePhase implements DependencyPhase { public final class ValidatePhase implements DependencyPhase {
@Override @Override
public ReadOnlyCollection<BuildingIssue> run(DependencyContext state) { public ReadOnlyCollection<BuildingIssue> run(DependencyContext ctx) {
if (state.root == null) { if (ctx.rootProjectId == null) {
final var issue = BuildingIssue final var issue = BuildingIssue
.builder() .builder()
.message("[DEPS]: root ProjectId not set") .error(true)
.message("[DEPS]: rootProjectId ProjectId not set")
.build(); .build();
return ReadOnlyCollection.wrap(List.of(issue)); return ReadOnlyCollection.wrap(List.of(issue));
} }
// Ensure the edges list matches the number of nodes // Ensure the dependenciesByProjectId list matches the number of project descriptors
if (state.dependenciesByProject.size() != state.projectNodes.size()) { if (ctx.dependenciesByProject.size() != ctx.projectDescriptors.size()) {
final var issue = BuildingIssue final var issue = BuildingIssue
.builder() .builder()
.message("[DEPS]: internal error: edges list size mismatch") .error(true)
.message("[DEPS]: internal error: dependenciesByProjectId list size mismatch")
.build(); .build();
return ReadOnlyCollection.wrap(List.of(issue)); 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(); 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 { dependencies {
api(libs.jackson.databind) api(libs.jackson.databind)
api(libs.apache.commons.lang3)
api(libs.apache.commons.collections) api(libs.apache.commons.collections)
} }

View File

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