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:
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user