This is an automated email from the ASF dual-hosted git repository. wusheng pushed a commit to branch fix/mal-list-of-and-test-split in repository https://gitbox.apache.org/repos/asf/skywalking.git
commit 03322bf76736fe0fb2ed108ec5b12c82a6f0daf3 Author: Wu Sheng <[email protected]> AuthorDate: Tue Mar 17 11:08:27 2026 +0800 Fix MAL List.of() varargs for >10 elements and split test class by scope --- .../analyzer/v2/compiler/MALClassGenerator.java | 10 +- .../v2/compiler/MALClassGeneratorClosureTest.java | 238 ++++++++++++++++++++ .../v2/compiler/MALClassGeneratorScopeTest.java | 243 +++++++++++++++++++++ .../v2/compiler/MALClassGeneratorTest.java | 240 +------------------- 4 files changed, 495 insertions(+), 236 deletions(-) diff --git a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java index cfaff7d829..465cbddbac 100644 --- a/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java +++ b/oap-server/analyzer/meter-analyzer/src/main/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGenerator.java @@ -919,18 +919,20 @@ public final class MALClassGenerator { } else if (arg instanceof MALExpressionModel.StringListArgument) { final List<String> vals = ((MALExpressionModel.StringListArgument) arg).getValues(); - sb.append("java.util.List.of("); + // Use Arrays.asList — Javassist cannot resolve List.of() varargs for >10 elements. + sb.append("java.util.Arrays.asList(new String[]{"); for (int i = 0; i < vals.size(); i++) { if (i > 0) { sb.append(", "); } sb.append('"').append(MALCodegenHelper.escapeJava(vals.get(i))).append('"'); } - sb.append(')'); + sb.append("})"); } else if (arg instanceof MALExpressionModel.NumberListArgument) { final List<Double> vals = ((MALExpressionModel.NumberListArgument) arg).getValues(); - sb.append("java.util.List.of("); + // Use Arrays.asList — Javassist cannot resolve List.of() varargs for >10 elements. + sb.append("java.util.Arrays.asList(new Number[]{"); for (int i = 0; i < vals.size(); i++) { if (i > 0) { sb.append(", "); @@ -942,7 +944,7 @@ public final class MALClassGenerator { sb.append("Double.valueOf(").append(v).append(')'); } } - sb.append(')'); + sb.append("})"); } else if (arg instanceof MALExpressionModel.BoolArgument) { sb.append(((MALExpressionModel.BoolArgument) arg).isValue()); } else if (arg instanceof MALExpressionModel.NullArgument) { diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorClosureTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorClosureTest.java new file mode 100644 index 0000000000..3dcbce0f6a --- /dev/null +++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorClosureTest.java @@ -0,0 +1,238 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.skywalking.oap.meter.analyzer.v2.compiler; + +import javassist.ClassPool; +import org.apache.skywalking.oap.meter.analyzer.v2.dsl.MalExpression; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Closure-related tests for MALClassGenerator: tag closures, forEach closures, + * ProcessRegistry FQCN resolution, network-profiling patterns, string concatenation, + * regex match, and time() scalar. + */ +class MALClassGeneratorClosureTest { + + private MALClassGenerator generator; + + @BeforeEach + void setUp() { + generator = new MALClassGenerator(new ClassPool(true)); + } + + // ==================== Closure key extraction tests ==================== + + @Test + void tagClosurePutsCorrectKey() throws Exception { + final MalExpression expr = generator.compile( + "test_key", + "metric.tag({tags -> tags.cluster = 'activemq::' + tags.cluster})"); + assertNotNull(expr); + final String source = generator.generateSource( + "metric.tag({tags -> tags.cluster = 'activemq::' + tags.cluster})"); + assertTrue(source.contains("this._tag"), + "Generated source should reference pre-compiled closure"); + } + + @Test + void tagClosureKeyExtractionViaGeneratedCode() throws Exception { + final MalExpression expr = generator.compile( + "test_key_gen", + "metric.tag({tags -> tags.service_name = 'svc1'})"); + assertNotNull(expr); + assertNotNull(expr.run(java.util.Map.of())); + } + + @Test + void tagClosureBracketAssignment() throws Exception { + final MalExpression expr = generator.compile( + "test_bracket", + "metric.tag({tags -> tags['my_key'] = 'my_value'})"); + assertNotNull(expr); + assertNotNull(expr.run(java.util.Map.of())); + } + + // ==================== forEach closure tests ==================== + + @Test + void forEachClosureCompiles() throws Exception { + final MalExpression expr = generator.compile( + "test_foreach", + "metric.forEach(['client', 'server'], {prefix, tags ->" + + " tags[prefix + '_name'] = 'value'})"); + assertNotNull(expr); + assertNotNull(expr.run(java.util.Map.of())); + } + + @Test + void forEachClosureWithBareReturn() throws Exception { + final MalExpression expr = generator.compile( + "test_foreach_return", + "metric.forEach(['x'], {prefix, tags ->\n" + + " if (tags[prefix + '_id'] != null) {\n" + + " return\n" + + " }\n" + + " tags[prefix + '_id'] = 'default'\n" + + "})"); + assertNotNull(expr); + assertNotNull(expr.run(java.util.Map.of())); + } + + @Test + void forEachClosureWithVarDeclAndElseIf() throws Exception { + final MalExpression expr = generator.compile( + "test_foreach_vars", + "metric.forEach(['component'], {key, tags ->\n" + + " String result = \"\"\n" + + " String protocol = tags['protocol']\n" + + " String ssl = tags['is_ssl']\n" + + " if (protocol == 'http' && ssl == 'true') {\n" + + " result = '129'\n" + + " } else if (protocol == 'http') {\n" + + " result = '49'\n" + + " } else if (ssl == 'true') {\n" + + " result = '130'\n" + + " } else {\n" + + " result = '110'\n" + + " }\n" + + " tags[key] = result\n" + + "})"); + assertNotNull(expr); + assertNotNull(expr.run(java.util.Map.of())); + } + + // ==================== ProcessRegistry FQCN resolution tests ==================== + + @Test + void processRegistryResolvedToFQCN() throws Exception { + final MalExpression expr = generator.compile( + "test_registry", + "metric.forEach(['client'], {prefix, tags ->\n" + + " tags[prefix + '_process_id'] = " + + "ProcessRegistry.generateVirtualLocalProcess(tags.service, tags.instance)\n" + + "})"); + assertNotNull(expr); + } + + // ==================== Network-profiling full expression tests ==================== + + @Test + void networkProfilingFirstClosureCompiles() throws Exception { + final MalExpression expr = generator.compile( + "test_np1", + "metric.forEach(['client', 'server'], { prefix, tags ->\n" + + " if (tags[prefix + '_process_id'] != null) {\n" + + " return\n" + + " }\n" + + " if (tags[prefix + '_local'] == 'true') {\n" + + " tags[prefix + '_process_id'] = ProcessRegistry" + + ".generateVirtualLocalProcess(tags.service, tags.instance)\n" + + " return\n" + + " }\n" + + " tags[prefix + '_process_id'] = ProcessRegistry" + + ".generateVirtualRemoteProcess(tags.service, tags.instance," + + " tags[prefix + '_address'])\n" + + " })"); + assertNotNull(expr); + } + + @Test + void networkProfilingSecondClosureCompiles() throws Exception { + final MalExpression expr = generator.compile( + "test_np2", + "metric.forEach(['component'], { key, tags ->\n" + + " String result = \"\"\n" + + " // protocol are defined in the component-libraries.yml\n" + + " String protocol = tags['protocol']\n" + + " String ssl = tags['is_ssl']\n" + + " if (protocol == 'http' && ssl == 'true') {\n" + + " result = '129'\n" + + " } else if (protocol == 'http') {\n" + + " result = '49'\n" + + " } else if (ssl == 'true') {\n" + + " result = '130'\n" + + " } else {\n" + + " result = '110'\n" + + " }\n" + + " tags[key] = result\n" + + " })"); + assertNotNull(expr); + } + + // ==================== String concatenation and regex in closures ==================== + + @Test + void apisixExpressionCompiles() throws Exception { + final MalExpression expr = generator.compile( + "test_apisix", + "metric.tag({tags -> tags.service_name = 'APISIX::'" + + "+(tags['skywalking_service']?.trim()?:'APISIX')})"); + assertNotNull(expr); + assertNotNull(expr.run(java.util.Map.of())); + } + + @Test + void closureStringConcatenation() throws Exception { + final MalExpression expr = generator.compile( + "test_concat", + "metric.tag({tags -> tags.service_name = 'APISIX::' + tags.service})"); + assertNotNull(expr); + assertNotNull(expr.run(java.util.Map.of())); + } + + @Test + void regexMatchWithDefCompiles() throws Exception { + final MalExpression expr = generator.compile( + "test_regex", + "metric.tag({tags ->\n" + + " def matcher = (tags.metrics_name =~ /\\.ssl\\.certificate\\.([^.]+)\\.expiration/)\n" + + " tags.secret_name = matcher ? matcher[0][1] : \"unknown\"\n" + + "})"); + assertNotNull(expr); + assertNotNull(expr.run(java.util.Map.of())); + } + + @Test + void envoyCAExpressionCompiles() throws Exception { + final MalExpression expr = generator.compile( + "test_envoy_ca", + "(metric.tagMatch('metrics_name', '.*ssl.*expiration_unix_time_seconds')" + + ".tag({tags ->\n" + + " def matcher = (tags.metrics_name =~ /\\.ssl\\.certificate\\.([^.]+)" + + "\\.expiration_unix_time_seconds/)\n" + + " tags.secret_name = matcher ? matcher[0][1] : \"unknown\"\n" + + "}).min(['app', 'secret_name']) - time())" + + ".downsampling(MIN).service(['app'], Layer.MESH_DP)"); + assertNotNull(expr); + } + + @Test + void timeScalarFunctionHandledInMetadata() throws Exception { + final MalExpression expr = generator.compile( + "test_time", + "(metric.sum(['app']) - time()).service(['app'], Layer.GENERAL)"); + assertNotNull(expr); + assertNotNull(expr.metadata()); + assertTrue(expr.metadata().getSamples().contains("metric")); + assertTrue(expr.metadata().getSamples().size() == 1); + } +} diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorScopeTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorScopeTest.java new file mode 100644 index 0000000000..2c994920d7 --- /dev/null +++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorScopeTest.java @@ -0,0 +1,243 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.skywalking.oap.meter.analyzer.v2.compiler; + +import javassist.ClassPool; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Full-expression compilation tests combining expPrefix + exp + expSuffix via formatExp. + * Covers endpoint, service, instance, serviceRelation scope suffixes, + * forEach closures with if/else-if/else chains, sum() with >10 label keys, + * and tag() closure chained with service() suffix. + */ +class MALClassGeneratorScopeTest { + + private MALClassGenerator generator; + + @BeforeEach + void setUp() { + generator = new MALClassGenerator(new ClassPool(true)); + } + + /** + * Simulates MetricConvert.formatExp() to build the full expression + * from expPrefix + exp + expSuffix. + */ + private static String formatExp(String expPrefix, String expSuffix, String exp) { + String ret = exp; + if (expPrefix != null && !expPrefix.isEmpty()) { + int dotIdx = exp.indexOf('.'); + if (dotIdx > 0) { + ret = String.format("(%s.%s)", exp.substring(0, dotIdx), expPrefix); + String after = exp.substring(dotIdx + 1); + if (!after.isEmpty()) { + ret = String.format("(%s.%s)", ret, after); + } + } + } + if (expSuffix != null && !expSuffix.isEmpty()) { + ret = String.format("(%s).%s", ret, expSuffix); + } + return ret; + } + + private void compileRule(String name, String expPrefix, String expSuffix, String exp) + throws Exception { + final String full = formatExp(expPrefix, expSuffix, exp); + assertNotNull(generator.compile(name, full)); + } + + // --- Shared constants --- + + private static final String FOREACH_COMPONENT_PREFIX = + "forEach(['component'], { key, tags ->\n" + + " String result = \"\"\n" + + " String protocol = tags['protocol']\n" + + " String ssl = tags['is_ssl']\n" + + " if (protocol == 'http' && ssl == 'true') {\n" + + " result = '129'\n" + + " } else if (protocol == 'http') {\n" + + " result = '49'\n" + + " } else if (ssl == 'true') {\n" + + " result = '130'\n" + + " } else {\n" + + " result = '110'\n" + + " }\n" + + " tags[key] = result\n" + + " })"; + + private static final String RELATION_SERVER_SUFFIX = + "serviceRelation(DetectPoint.SERVER, " + + "[\"client_service_subset\", \"client_service_name\", \"client_namespace\", \"client_service_cluster\", \"client_service_env\"], " + + "[\"server_service_subset\", \"server_service_name\", \"server_namespace\", \"server_service_cluster\", \"server_service_env\"], " + + "'|', Layer.GENERAL, 'component')"; + + private static final String RELATION_CLIENT_SUFFIX = + "serviceRelation(DetectPoint.CLIENT, " + + "[\"client_service_subset\", \"client_service_name\", \"client_namespace\", \"client_service_cluster\", \"client_service_env\"], " + + "[\"server_service_subset\", \"server_service_name\", \"server_namespace\", \"server_service_cluster\", \"server_service_env\"], " + + "'|', Layer.GENERAL, 'component')"; + + private static final String HISTOGRAM_12_LABELS = + ".sum([\"le\", \"client_service_subset\", \"client_service_name\", \"client_namespace\", " + + "\"client_service_cluster\", \"client_service_env\", \"server_service_subset\", " + + "\"server_service_name\", \"server_namespace\", \"server_service_cluster\", " + + "\"server_service_env\", 'component'])" + + ".downsampling(SUM).histogram().histogram_percentile([50, 75, 90, 95, 99])"; + + // ==================== endpoint scope ==================== + + @Test + void endpointScopeWithHistogramPercentile() throws Exception { + final String suffix = "endpoint([\"service_subset\", \"service_name\", \"k8s_namespace\", \"service_cluster\", \"service_env\"], [\"api\"], \"|\", Layer.GENERAL)"; + compileRule("ep_cpm", null, suffix, + "api_agent_api_total_count.downsampling(SUM_PER_MIN)"); + compileRule("ep_duration", null, suffix, + "api_agent_api_total_duration.downsampling(SUM_PER_MIN)"); + compileRule("ep_success", null, suffix, + "api_agent_api_http_success_count.downsampling(SUM_PER_MIN)"); + compileRule("ep_percentile", null, suffix, + "api_agent_api_response_time_histogram" + + ".sum([\"le\", \"service_subset\", \"service_name\", \"k8s_namespace\", \"service_cluster\", \"service_env\", \"api\"])" + + ".downsampling(SUM).histogram().histogram_percentile([50, 75, 90, 95, 99])"); + } + + // ==================== service scope ==================== + + @Test + void serviceScopeWithDelimiter() throws Exception { + final String suffix = "service([\"service_subset\", \"service_name\", \"k8s_namespace\", \"service_cluster\", \"service_env\"], \"|\", Layer.GENERAL)"; + compileRule("svc_api_cpm", null, suffix, + "api_agent_api_total_count.downsampling(SUM_PER_MIN)"); + compileRule("svc_api_duration", null, suffix, + "api_agent_api_total_duration.downsampling(SUM_PER_MIN)"); + compileRule("svc_api_success", null, suffix, + "api_agent_api_http_success_count.downsampling(SUM_PER_MIN)"); + compileRule("svc_api_percentile", null, suffix, + "api_agent_api_response_time_histogram" + + ".sum([\"le\", \"service_subset\", \"service_name\", \"k8s_namespace\", \"service_cluster\", \"service_env\"])" + + ".downsampling(SUM).histogram().histogram_percentile([50, 75, 90, 95, 99])"); + } + + @Test + void serviceScopeWithTcpMetrics() throws Exception { + final String suffix = "service([\"service_subset\", \"service_name\", \"k8s_namespace\", \"service_cluster\", \"service_env\"], \"|\", Layer.GENERAL)"; + for (final String metric : new String[]{ + "write_bytes", "write_count", "write_execute_time", + "read_bytes", "read_count", "read_execute_time", + "connect_connection_count", "connect_connection_time", + "accept_connection_count", "accept_connection_time", + "close_connection_count", "close_connection_time"}) { + compileRule("svc_tcp_" + metric, null, suffix, + "metric_tcp_" + metric + ".downsampling(SUM_PER_MIN)"); + } + } + + @Test + void tagClosureChainedWithServiceSuffix() throws Exception { + final String suffix = "tag({tags -> tags.service = 'satellite::' + tags.service}).service(['service'], Layer.SO11Y_SATELLITE)"; + compileRule("sat_receive", null, suffix, + "sw_stl_gatherer_receive_count.sum([\"pipe\", \"status\", \"service\"]).increase(\"PT1M\")"); + compileRule("sat_fetch", null, suffix, + "sw_stl_gatherer_fetch_count.sum([\"pipe\", \"status\", \"service\"]).increase(\"PT1M\")"); + compileRule("sat_queue_in", null, suffix, + "sw_stl_queue_output_count.sum([\"pipe\", \"status\", \"service\"]).increase(\"PT1M\")"); + compileRule("sat_send", null, suffix, + "sw_stl_sender_output_count.sum([\"pipe\", \"status\", \"service\"]).increase(\"PT1M\")"); + compileRule("sat_queue_cap", null, suffix, + "sw_stl_pipeline_queue_total_capacity.sum([\"pipeline\", \"service\"])"); + compileRule("sat_queue_used", null, suffix, + "sw_stl_pipeline_queue_partition_size.sum([\"pipeline\", \"service\"])"); + compileRule("sat_cpu", null, suffix, + "sw_stl_grpc_server_cpu_gauge.downsampling(LATEST)"); + compileRule("sat_conn", null, suffix, + "sw_stl_grpc_server_connection_count.downsampling(LATEST)"); + } + + // ==================== instance scope ==================== + + @Test + void instanceScopeWithNullLayerKey() throws Exception { + final String suffix = "instance([\"service_subset\", \"service_name\", \"k8s_namespace\", \"service_cluster\", \"service_env\"], \"|\", [\"instance_name\"], \"|\", Layer.GENERAL, null)"; + for (final String metric : new String[]{ + "write_bytes", "write_count", "write_execute_time", + "read_bytes", "read_count", "read_execute_time", + "connect_connection_count", "connect_connection_time", + "accept_connection_count", "accept_connection_time", + "close_connection_count", "close_connection_time"}) { + compileRule("inst_tcp_" + metric, null, suffix, + "api_agent_tcp_" + metric + ".downsampling(SUM_PER_MIN)"); + } + } + + // ==================== serviceRelation scope ==================== + + @Test + void serviceRelation6ArgWithForEachServer() throws Exception { + compileRule("rel_api_srv_cpm", FOREACH_COMPONENT_PREFIX, RELATION_SERVER_SUFFIX, + "metric_server_api_total_count.downsampling(SUM_PER_MIN)"); + compileRule("rel_api_srv_dur", FOREACH_COMPONENT_PREFIX, RELATION_SERVER_SUFFIX, + "metric_server_api_total_duration.downsampling(SUM_PER_MIN)"); + compileRule("rel_api_srv_suc", FOREACH_COMPONENT_PREFIX, RELATION_SERVER_SUFFIX, + "metric_server_api_http_success_count.downsampling(SUM_PER_MIN)"); + compileRule("rel_api_srv_pct", FOREACH_COMPONENT_PREFIX, RELATION_SERVER_SUFFIX, + "metric_server_api_response_time_histogram" + HISTOGRAM_12_LABELS); + } + + @Test + void serviceRelation6ArgWithForEachClient() throws Exception { + compileRule("rel_api_cli_cpm", FOREACH_COMPONENT_PREFIX, RELATION_CLIENT_SUFFIX, + "metric_client_api_total_count.downsampling(SUM_PER_MIN)"); + compileRule("rel_api_cli_dur", FOREACH_COMPONENT_PREFIX, RELATION_CLIENT_SUFFIX, + "metric_client_api_total_duration.downsampling(SUM_PER_MIN)"); + compileRule("rel_api_cli_suc", FOREACH_COMPONENT_PREFIX, RELATION_CLIENT_SUFFIX, + "metric_client_api_http_success_count.downsampling(SUM_PER_MIN)"); + compileRule("rel_api_cli_pct", FOREACH_COMPONENT_PREFIX, RELATION_CLIENT_SUFFIX, + "metric_client_api_response_time_histogram" + HISTOGRAM_12_LABELS); + } + + @Test + void serviceRelationTcpWithForEachServer() throws Exception { + for (final String metric : new String[]{ + "write_bytes", "write_count", "write_execute_time", + "read_bytes", "read_count", "read_execute_time", + "connect_connection_count", "connect_connection_time", + "accept_connection_count", "accept_connection_time", + "close_connection_count", "close_connection_time"}) { + compileRule("rel_tcp_srv_" + metric, FOREACH_COMPONENT_PREFIX, RELATION_SERVER_SUFFIX, + "metric_server_tcp_" + metric + ".downsampling(SUM_PER_MIN)"); + } + } + + @Test + void serviceRelationTcpWithForEachClient() throws Exception { + for (final String metric : new String[]{ + "write_bytes", "write_count", "write_execute_time", + "read_bytes", "read_count", "read_execute_time", + "connect_connection_count", "connect_connection_time", + "accept_connection_count", "accept_connection_time", + "close_connection_count", "close_connection_time"}) { + compileRule("rel_tcp_cli_" + metric, FOREACH_COMPONENT_PREFIX, RELATION_CLIENT_SUFFIX, + "metric_client_tcp_" + metric + ".downsampling(SUM_PER_MIN)"); + } + } +} diff --git a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorTest.java b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorTest.java index a850977a01..8bd2d4c179 100644 --- a/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorTest.java +++ b/oap-server/analyzer/meter-analyzer/src/test/java/org/apache/skywalking/oap/meter/analyzer/v2/compiler/MALClassGeneratorTest.java @@ -26,6 +26,10 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +/** + * Basic compilation, error handling, source generation, and bytecode verification + * tests for MALClassGenerator. + */ class MALClassGeneratorTest { private MALClassGenerator generator; @@ -35,12 +39,13 @@ class MALClassGeneratorTest { generator = new MALClassGenerator(new ClassPool(true)); } + // ==================== Basic compilation tests ==================== + @Test void compileSimpleMetric() throws Exception { final MalExpression expr = generator.compile( "test_metric", "instance_jvm_cpu"); assertNotNull(expr); - // Run returns SampleFamily.EMPTY since no samples are provided assertNotNull(expr.run(java.util.Map.of())); } @@ -110,9 +115,7 @@ class MALClassGeneratorTest { final String source = generator.generateSource( "instance_jvm_cpu.sum(['service'])"); assertNotNull(source); - // Generated source should contain getOrDefault for the metric - org.junit.jupiter.api.Assertions.assertTrue( - source.contains("getOrDefault")); + assertTrue(source.contains("getOrDefault")); } @Test @@ -149,260 +152,37 @@ class MALClassGeneratorTest { @Test void emptyExpressionThrows() { - // Demo error: MAL expression parsing failed: 1:0 mismatched input '<EOF>' - // expecting {IDENTIFIER, NUMBER, '(', '-'} assertThrows(Exception.class, () -> generator.compile("test", "")); } @Test void malformedExpressionThrows() { - // Demo error: MAL expression parsing failed: 1:7 token recognition error at: '@' assertThrows(Exception.class, () -> generator.compile("test", "metric.@invalid")); } @Test void unclosedParenthesisThrows() { - // Demo error: MAL expression parsing failed: 1:8 mismatched input '<EOF>' - // expecting {')', '+', '-', '*', '/'} assertThrows(Exception.class, () -> generator.compile("test", "(metric1 ")); } @Test void invalidFilterClosureThrows() { - // Demo error: MAL filter parsing failed: 1:0 mismatched input 'invalid' - // expecting '{' assertThrows(Exception.class, () -> generator.compileFilter("invalid filter")); } @Test void emptyFilterBodyThrows() { - // Demo error: MAL filter parsing failed: 1:1 mismatched input '}' - // expecting {IDENTIFIER, ...} assertThrows(Exception.class, () -> generator.compileFilter("{ }")); } - // ==================== Closure key extraction tests ==================== - - @Test - void tagClosurePutsCorrectKey() throws Exception { - // Issue: tags.cluster = expr should generate tags.put("cluster", ...) - // NOT tags.put("tags.cluster", ...) - final MalExpression expr = generator.compile( - "test_key", - "metric.tag({tags -> tags.cluster = 'activemq::' + tags.cluster})"); - assertNotNull(expr); - final String source = generator.generateSource( - "metric.tag({tags -> tags.cluster = 'activemq::' + tags.cluster})"); - assertTrue(source.contains("this._tag"), - "Generated source should reference pre-compiled closure"); - } - - @Test - void tagClosureKeyExtractionViaGeneratedCode() throws Exception { - // Verify the closure generates correct put("cluster", ...) not put("tags.cluster", ...) - final MalExpression expr = generator.compile( - "test_key_gen", - "metric.tag({tags -> tags.service_name = 'svc1'})"); - assertNotNull(expr); - assertNotNull(expr.run(java.util.Map.of())); - } - - @Test - void tagClosureBracketAssignment() throws Exception { - // tags['key_name'] = 'value' should also use correct key - final MalExpression expr = generator.compile( - "test_bracket", - "metric.tag({tags -> tags['my_key'] = 'my_value'})"); - assertNotNull(expr); - assertNotNull(expr.run(java.util.Map.of())); - } - - // ==================== forEach closure tests ==================== - - @Test - void forEachClosureCompiles() throws Exception { - // forEach requires ForEachFunction.accept(String, Map), not TagFunction.apply(Map) - final MalExpression expr = generator.compile( - "test_foreach", - "metric.forEach(['client', 'server'], {prefix, tags ->" - + " tags[prefix + '_name'] = 'value'})"); - assertNotNull(expr); - assertNotNull(expr.run(java.util.Map.of())); - } - - @Test - void forEachClosureWithBareReturn() throws Exception { - // forEach with bare return (void method) — should not throw - final MalExpression expr = generator.compile( - "test_foreach_return", - "metric.forEach(['x'], {prefix, tags ->\n" - + " if (tags[prefix + '_id'] != null) {\n" - + " return\n" - + " }\n" - + " tags[prefix + '_id'] = 'default'\n" - + "})"); - assertNotNull(expr); - assertNotNull(expr.run(java.util.Map.of())); - } - - @Test - void forEachClosureWithVarDeclAndElseIf() throws Exception { - // Full pattern from network-profiling.yaml second closure - final MalExpression expr = generator.compile( - "test_foreach_vars", - "metric.forEach(['component'], {key, tags ->\n" - + " String result = \"\"\n" - + " String protocol = tags['protocol']\n" - + " String ssl = tags['is_ssl']\n" - + " if (protocol == 'http' && ssl == 'true') {\n" - + " result = '129'\n" - + " } else if (protocol == 'http') {\n" - + " result = '49'\n" - + " } else if (ssl == 'true') {\n" - + " result = '130'\n" - + " } else {\n" - + " result = '110'\n" - + " }\n" - + " tags[key] = result\n" - + "})"); - assertNotNull(expr); - assertNotNull(expr.run(java.util.Map.of())); - } - - // ==================== ProcessRegistry FQCN resolution tests ==================== - - @Test - void processRegistryResolvedToFQCN() throws Exception { - // ProcessRegistry.generateVirtualLocalProcess() should resolve to FQCN - final MalExpression expr = generator.compile( - "test_registry", - "metric.forEach(['client'], {prefix, tags ->\n" - + " tags[prefix + '_process_id'] = " - + "ProcessRegistry.generateVirtualLocalProcess(tags.service, tags.instance)\n" - + "})"); - assertNotNull(expr); - // We can't easily execute this (needs ProcessRegistry runtime) but compile should succeed - } - - // ==================== Network-profiling full expression tests ==================== - - @Test - void networkProfilingFirstClosureCompiles() throws Exception { - // Full first closure from network-profiling.yaml expPrefix - final MalExpression expr = generator.compile( - "test_np1", - "metric.forEach(['client', 'server'], { prefix, tags ->\n" - + " if (tags[prefix + '_process_id'] != null) {\n" - + " return\n" - + " }\n" - + " if (tags[prefix + '_local'] == 'true') {\n" - + " tags[prefix + '_process_id'] = ProcessRegistry" - + ".generateVirtualLocalProcess(tags.service, tags.instance)\n" - + " return\n" - + " }\n" - + " tags[prefix + '_process_id'] = ProcessRegistry" - + ".generateVirtualRemoteProcess(tags.service, tags.instance," - + " tags[prefix + '_address'])\n" - + " })"); - assertNotNull(expr); - } - - @Test - void networkProfilingSecondClosureCompiles() throws Exception { - // Full second closure from network-profiling.yaml expPrefix - final MalExpression expr = generator.compile( - "test_np2", - "metric.forEach(['component'], { key, tags ->\n" - + " String result = \"\"\n" - + " // protocol are defined in the component-libraries.yml\n" - + " String protocol = tags['protocol']\n" - + " String ssl = tags['is_ssl']\n" - + " if (protocol == 'http' && ssl == 'true') {\n" - + " result = '129'\n" - + " } else if (protocol == 'http') {\n" - + " result = '49'\n" - + " } else if (ssl == 'true') {\n" - + " result = '130'\n" - + " } else {\n" - + " result = '110'\n" - + " }\n" - + " tags[key] = result\n" - + " })"); - assertNotNull(expr); - } - - // ==================== String concatenation in closures ==================== - - @Test - void apisixExpressionCompiles() throws Exception { - // The APISIX expression that originally triggered the E2E failure: - // safe navigation + elvis + bracket access + string concat - final MalExpression expr = generator.compile( - "test_apisix", - "metric.tag({tags -> tags.service_name = 'APISIX::'" - + "+(tags['skywalking_service']?.trim()?:'APISIX')})"); - assertNotNull(expr); - assertNotNull(expr.run(java.util.Map.of())); - } - - @Test - void closureStringConcatenation() throws Exception { - // APISIX-style: tags.service_name = 'APISIX::' + tags.service - final MalExpression expr = generator.compile( - "test_concat", - "metric.tag({tags -> tags.service_name = 'APISIX::' + tags.service})"); - assertNotNull(expr); - assertNotNull(expr.run(java.util.Map.of())); - } - - @Test - void regexMatchWithDefCompiles() throws Exception { - // envoy-ca pattern: def + regex match + ternary with chained indexing - final MalExpression expr = generator.compile( - "test_regex", - "metric.tag({tags ->\n" - + " def matcher = (tags.metrics_name =~ /\\.ssl\\.certificate\\.([^.]+)\\.expiration/)\n" - + " tags.secret_name = matcher ? matcher[0][1] : \"unknown\"\n" - + "})"); - assertNotNull(expr); - assertNotNull(expr.run(java.util.Map.of())); - } - - @Test - void envoyCAExpressionCompiles() throws Exception { - // Full envoy-ca.yaml expression with regex closure, subtraction of time(), and service - final MalExpression expr = generator.compile( - "test_envoy_ca", - "(metric.tagMatch('metrics_name', '.*ssl.*expiration_unix_time_seconds')" - + ".tag({tags ->\n" - + " def matcher = (tags.metrics_name =~ /\\.ssl\\.certificate\\.([^.]+)" - + "\\.expiration_unix_time_seconds/)\n" - + " tags.secret_name = matcher ? matcher[0][1] : \"unknown\"\n" - + "}).min(['app', 'secret_name']) - time())" - + ".downsampling(MIN).service(['app'], Layer.MESH_DP)"); - assertNotNull(expr); - } - - @Test - void timeScalarFunctionHandledInMetadata() throws Exception { - // time() should not appear as a sample name and should be treated as scalar - final MalExpression expr = generator.compile( - "test_time", - "(metric.sum(['app']) - time()).service(['app'], Layer.GENERAL)"); - assertNotNull(expr); - assertNotNull(expr.metadata()); - // time() should not be in sample names - assertTrue(expr.metadata().getSamples().contains("metric")); - assertTrue(expr.metadata().getSamples().size() == 1); - } + // ==================== Bytecode verification ==================== @Test void runMethodHasLocalVariableTable() throws Exception { - // Compile a class that writes its .class file for inspection final java.io.File tmpDir = java.nio.file.Files.createTempDirectory("mal-lvt").toFile(); try { final ClassPool pool = new ClassPool(true); @@ -411,11 +191,9 @@ class MALClassGeneratorTest { final MalExpression expr = gen.compile( "test_lvt", "instance_jvm_cpu.sum(['service', 'instance'])"); assertNotNull(expr); - // Read the .class file bytecode and verify LVT final java.io.File[] classFiles = tmpDir.listFiles((d, n) -> n.endsWith(".class")); assertNotNull(classFiles); assertTrue(classFiles.length > 0, "Should have generated .class file"); - // Use javassist to read back and check for LocalVariableTable final javassist.bytecode.ClassFile cf = new javassist.bytecode.ClassFile( new java.io.DataInputStream( @@ -428,7 +206,6 @@ class MALClassGeneratorTest { (javassist.bytecode.LocalVariableAttribute) code.getAttribute(javassist.bytecode.LocalVariableAttribute.tag); assertNotNull(lva, "run() should have LocalVariableTable attribute"); - // Check that slot 1 has name "samples" boolean foundSamples = false; boolean foundSf = false; for (int i = 0; i < lva.tableLength(); i++) { @@ -449,5 +226,4 @@ class MALClassGeneratorTest { tmpDir.delete(); } } - }
