- 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.
260 lines
8.5 KiB
C#
260 lines
8.5 KiB
C#
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
|
|
};
|
|
}
|
|
}
|
|
|