feat: add stella-callgraph-node for JavaScript/TypeScript call graph extraction

- Implemented a new tool `stella-callgraph-node` that extracts call graphs from JavaScript/TypeScript projects using Babel AST.
- Added command-line interface with options for JSON output and help.
- Included functionality to analyze project structure, detect functions, and build call graphs.
- Created a package.json file for dependency management.

feat: introduce stella-callgraph-python for Python call graph extraction

- Developed `stella-callgraph-python` to extract call graphs from Python projects using AST analysis.
- Implemented command-line interface with options for JSON output and verbose logging.
- Added framework detection to identify popular web frameworks and their entry points.
- Created an AST analyzer to traverse Python code and extract function definitions and calls.
- Included requirements.txt for project dependencies.

chore: add framework detection for Python projects

- Implemented framework detection logic to identify frameworks like Flask, FastAPI, Django, and others based on project files and import patterns.
- Enhanced the AST analyzer to recognize entry points based on decorators and function definitions.
This commit is contained in:
master
2025-12-19 18:11:59 +02:00
parent 951a38d561
commit 8779e9226f
130 changed files with 19011 additions and 422 deletions

View File

@@ -0,0 +1,259 @@
using FluentAssertions;
using Microsoft.Extensions.Options;
using StellaOps.Router.Common.Enums;
using StellaOps.Router.Common.Models;
using StellaOps.Router.Gateway.Configuration;
using StellaOps.Router.Gateway.Routing;
using Xunit;
namespace StellaOps.Router.Gateway.Tests;
public sealed class DefaultRoutingPluginTests
{
[Fact]
public async Task ChooseInstanceAsync_WhenNoCandidates_ReturnsNull()
{
var plugin = CreatePlugin(gatewayRegion: "eu1");
var decision = await plugin.ChooseInstanceAsync(
new RoutingContext
{
Method = "GET",
Path = "/items",
Headers = new Dictionary<string, string>(),
Endpoint = new EndpointDescriptor
{
ServiceName = "inventory",
Version = "1.0.0",
Method = "GET",
Path = "/items"
},
AvailableConnections = [],
GatewayRegion = "eu1",
RequestedVersion = null,
CancellationToken = CancellationToken.None
},
CancellationToken.None);
decision.Should().BeNull();
}
[Fact]
public async Task ChooseInstanceAsync_WhenRequestedVersionDoesNotMatch_ReturnsNull()
{
var plugin = CreatePlugin(
gatewayRegion: "eu1",
options: new RoutingOptions
{
StrictVersionMatching = true,
DefaultVersion = null
});
var candidates = new List<ConnectionState>
{
CreateConnection("inv-1", "inventory", "1.0.0", region: "eu1", status: InstanceHealthStatus.Healthy)
};
var decision = await plugin.ChooseInstanceAsync(
CreateContext(
requestedVersion: "2.0.0",
candidates: candidates),
CancellationToken.None);
decision.Should().BeNull();
}
[Fact]
public async Task ChooseInstanceAsync_PrefersHealthyOverDegraded()
{
var plugin = CreatePlugin(
gatewayRegion: "eu1",
options: new RoutingOptions
{
StrictVersionMatching = true,
DefaultVersion = null,
PreferLocalRegion = true,
AllowDegradedInstances = true,
TieBreaker = TieBreakerMode.RoundRobin
});
var degraded = CreateConnection("inv-degraded", "inventory", "1.0.0", region: "eu1", status: InstanceHealthStatus.Degraded);
degraded.AveragePingMs = 1;
var healthy = CreateConnection("inv-healthy", "inventory", "1.0.0", region: "eu1", status: InstanceHealthStatus.Healthy);
healthy.AveragePingMs = 50;
var decision = await plugin.ChooseInstanceAsync(
CreateContext(
requestedVersion: "1.0.0",
candidates: [degraded, healthy]),
CancellationToken.None);
decision.Should().NotBeNull();
decision!.Connection.ConnectionId.Should().Be("inv-healthy");
}
[Fact]
public async Task ChooseInstanceAsync_PrefersLocalRegionOverRemote()
{
var plugin = CreatePlugin(
gatewayRegion: "eu1",
options: new RoutingOptions
{
StrictVersionMatching = true,
DefaultVersion = null,
PreferLocalRegion = true,
AllowDegradedInstances = true,
TieBreaker = TieBreakerMode.RoundRobin
});
var remote = CreateConnection("inv-us1", "inventory", "1.0.0", region: "us1", status: InstanceHealthStatus.Healthy);
var local = CreateConnection("inv-eu1", "inventory", "1.0.0", region: "eu1", status: InstanceHealthStatus.Healthy);
var decision = await plugin.ChooseInstanceAsync(
CreateContext(
requestedVersion: "1.0.0",
candidates: [remote, local]),
CancellationToken.None);
decision.Should().NotBeNull();
decision!.Connection.ConnectionId.Should().Be("inv-eu1");
}
[Fact]
public async Task ChooseInstanceAsync_WhenNoLocal_UsesNeighborRegion()
{
var plugin = CreatePlugin(
gatewayRegion: "eu1",
gatewayNeighbors: ["eu2"],
options: new RoutingOptions
{
StrictVersionMatching = true,
DefaultVersion = null,
PreferLocalRegion = true,
AllowDegradedInstances = true,
TieBreaker = TieBreakerMode.RoundRobin
});
var neighbor = CreateConnection("inv-eu2", "inventory", "1.0.0", region: "eu2", status: InstanceHealthStatus.Healthy);
var remote = CreateConnection("inv-us1", "inventory", "1.0.0", region: "us1", status: InstanceHealthStatus.Healthy);
var decision = await plugin.ChooseInstanceAsync(
CreateContext(
requestedVersion: "1.0.0",
candidates: [remote, neighbor]),
CancellationToken.None);
decision.Should().NotBeNull();
decision!.Connection.ConnectionId.Should().Be("inv-eu2");
}
[Fact]
public async Task ChooseInstanceAsync_WhenTied_UsesRoundRobin()
{
var plugin = CreatePlugin(
gatewayRegion: "eu1",
options: new RoutingOptions
{
StrictVersionMatching = true,
DefaultVersion = null,
PreferLocalRegion = false,
AllowDegradedInstances = true,
TieBreaker = TieBreakerMode.RoundRobin,
PingToleranceMs = 1_000
});
var heartbeat = DateTime.UtcNow;
var a = CreateConnection("inv-a", "inventory", "1.0.0", region: "us1", status: InstanceHealthStatus.Healthy);
a.AveragePingMs = 10;
a.LastHeartbeatUtc = heartbeat;
var b = CreateConnection("inv-b", "inventory", "1.0.0", region: "us1", status: InstanceHealthStatus.Healthy);
b.AveragePingMs = 10;
b.LastHeartbeatUtc = heartbeat;
var ctx = CreateContext(requestedVersion: "1.0.0", candidates: [a, b]);
var decision1 = await plugin.ChooseInstanceAsync(ctx, CancellationToken.None);
var decision2 = await plugin.ChooseInstanceAsync(ctx, CancellationToken.None);
decision1.Should().NotBeNull();
decision2.Should().NotBeNull();
decision1!.Connection.ConnectionId.Should().Be("inv-a");
decision2!.Connection.ConnectionId.Should().Be("inv-b");
}
private static DefaultRoutingPlugin CreatePlugin(
string gatewayRegion,
RoutingOptions? options = null,
IReadOnlyList<string>? gatewayNeighbors = null)
{
options ??= new RoutingOptions
{
StrictVersionMatching = true,
DefaultVersion = null,
PreferLocalRegion = true,
AllowDegradedInstances = true,
TieBreaker = TieBreakerMode.RoundRobin
};
var node = new RouterNodeConfig
{
Region = gatewayRegion,
NeighborRegions = gatewayNeighbors?.ToList() ?? []
};
return new DefaultRoutingPlugin(
Options.Create(options),
Options.Create(node));
}
private static RoutingContext CreateContext(
string? requestedVersion,
IReadOnlyList<ConnectionState> candidates)
{
return new RoutingContext
{
Method = "GET",
Path = "/items",
Headers = new Dictionary<string, string>(),
Endpoint = new EndpointDescriptor
{
ServiceName = "inventory",
Version = "1.0.0",
Method = "GET",
Path = "/items"
},
AvailableConnections = candidates,
GatewayRegion = "eu1",
RequestedVersion = requestedVersion,
CancellationToken = CancellationToken.None
};
}
private static ConnectionState CreateConnection(
string connectionId,
string serviceName,
string version,
string region,
InstanceHealthStatus status)
{
return new ConnectionState
{
ConnectionId = connectionId,
Instance = new InstanceDescriptor
{
InstanceId = connectionId,
ServiceName = serviceName,
Version = version,
Region = region
},
Status = status,
LastHeartbeatUtc = DateTime.UtcNow,
TransportType = TransportType.InMemory
};
}
}