This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/texera.git
commit 140de712b17333c5f004db6760d0f3409a3e5526 Author: Matthew B. <[email protected]> AuthorDate: Wed Jul 22 12:19:59 2026 -0700 test(workflow-operator): add unit tests for PythonCodegenBase (#6663) ### What changes were proposed in this PR? - Add PythonCodegenBaseSpec covering the shared ProcessTableOperator template skeleton and helper methods emitted by render. - Verify the per-task codegen payload and parse snippets are spliced in verbatim and are codegen-driven, not hardcoded. - Verify numeric context fields render as raw Python literals while user strings render as runtime decode expressions, with a leakage guard. ### Any related issues, documentation, discussions? Closes: #6662 ### How was this PR tested? - Run: `sbt "WorkflowOperator/testOnly *PythonCodegenBaseSpec"`, expect all 6 tests passing. - Test-only change; no production code is modified. ### Was this PR authored or co-authored using generative AI tooling? Co-authored with Claude Opus 4.8 in compliance with ASF --------- Co-authored-by: Claude Opus 4.8 <[email protected]> --- .../codegen/PythonCodegenBaseSpec.scala | 146 +++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/PythonCodegenBaseSpec.scala b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/PythonCodegenBaseSpec.scala new file mode 100644 index 0000000000..3705fad47a --- /dev/null +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/huggingFace/codegen/PythonCodegenBaseSpec.scala @@ -0,0 +1,146 @@ +/* + * 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.texera.amber.operator.huggingFace.codegen + +import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableString +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class PythonCodegenBaseSpec extends AnyFlatSpec with Matchers { + + // A stand-in TaskCodegen whose payload/parse snippets are distinctive + // sentinels. Because payloadPython/parsePython return plain Python source + // fragments (not EncodableString user data), the base template must splice + // them in verbatim, letting the spec prove the splice happened and that the + // fragment is codegen-driven rather than hardcoded in the base. + private object StubCodegen extends TaskCodegen { + override def task: String = "text-generation" + override def payloadPython(ctx: CodegenContext): String = + " payload = STUB_PAYLOAD_zX7q42" + override def parsePython(ctx: CodegenContext): String = + " return STUB_PARSE_zX7q42" + } + + private def makeCtx( + hfApiToken: EncodableString = "token", + modelId: EncodableString = "Qwen/Qwen2.5-72B-Instruct", + promptColumn: EncodableString = "prompt", + resultColumn: EncodableString = "hf_response", + task: EncodableString = "text-generation", + systemPrompt: EncodableString = "You are a helpful assistant.", + safeMaxTokens: Int = 256, + safeTemp: Double = 0.7 + ): CodegenContext = + CodegenContext( + hfApiToken = hfApiToken, + modelId = modelId, + promptColumn = promptColumn, + resultColumn = resultColumn, + task = task, + systemPrompt = systemPrompt, + safeMaxTokens = safeMaxTokens, + safeTemp = safeTemp + ) + + "PythonCodegenBase.render" should "emit the ProcessTableOperator skeleton and shared helpers" in { + val out = PythonCodegenBase.render(makeCtx(), StubCodegen) + out should include("class ProcessTableOperator(UDFTableOperator):") + out should include("def open(self):") + out should include("def process_table(") + out should include("def _parse_response(") + out should include("def _resolve_providers(") + out should include("def _post_with_fallback(") + out should include("PROVIDER_COST_PRIORITY") + out should include("CHAT_ROUTES") + out should include("MAX_REMOTE_FETCH_BYTES") + } + + it should "splice the per-task codegen's payload and parse snippets into the template" in { + val out = PythonCodegenBase.render(makeCtx(), StubCodegen) + out should include("STUB_PAYLOAD_zX7q42") + out should include("STUB_PARSE_zX7q42") + } + + it should "delegate payload/parse to the codegen rather than hardcoding a task's output" in { + // The same context routed through two different codegens must yield the + // spliced fragments of whichever codegen is passed, proving the base is a + // pure host for the codegen's snippets. + val stub = PythonCodegenBase.render(makeCtx(), StubCodegen) + val real = PythonCodegenBase.render(makeCtx(), TextGenCodegen) + stub should include("STUB_PAYLOAD_zX7q42") + real should not include "STUB_PAYLOAD_zX7q42" + // TextGenCodegen's real payload/parse markers appear only in the real run. + // Use its system-role message, unique to TextGenCodegen: the shared base + // template contains "messages" in other helper branches, so that alone + // would not prove TextGenCodegen's payload was spliced in. + real should include("""{"role": "system", "content": self.SYSTEM_PROMPT}""") + real should include("max_tokens") + real should include("choices") + } + + it should "interpolate the numeric context fields as raw Python literals" in { + val out = PythonCodegenBase.render(makeCtx(safeMaxTokens = 512, safeTemp = 0.9), StubCodegen) + out should include("self.MAX_NEW_TOKENS = 512") + out should include("self.TEMPERATURE = 0.9") + } + + it should "assign user-provided strings via runtime base64 decode expressions, not raw literals" in { + val out = PythonCodegenBase.render(makeCtx(), StubCodegen) + // Every user-supplied string field open() assigns is set through the safe + // decode helper. This covers all EncodableString context fields, including + // the result/task and per-task (image/audio/context/labels/sentences) + // fields, not just the text-generation ones. + out should include("self.HF_API_TOKEN = self.decode_python_template(") + out should include("self.MODEL_ID = self.decode_python_template(") + out should include("self.PROMPT_COLUMN = self.decode_python_template(") + out should include("self.RESULT_COLUMN = self.decode_python_template(") + out should include("self.TASK = self.decode_python_template(") + out should include("self.SYSTEM_PROMPT = self.decode_python_template(") + out should include("self.IMAGE_INPUT = self.decode_python_template(") + out should include("self.INPUT_IMAGE_COLUMN = self.decode_python_template(") + out should include("self.AUDIO_INPUT = self.decode_python_template(") + out should include("self.INPUT_AUDIO_COLUMN = self.decode_python_template(") + out should include("self.CONTEXT_COLUMN = self.decode_python_template(") + out should include("self.CANDIDATE_LABELS = self.decode_python_template(") + out should include("self.SENTENCES_COLUMN = self.decode_python_template(") + } + + it should "never leak raw user-provided string values into the generated source" in { + // Sentinels contain underscores, which base64 output cannot contain, so a + // literal match here can only mean the raw value leaked past the encoder. + val out = PythonCodegenBase.render( + makeCtx( + hfApiToken = "MARKER_TOKEN_zXyq42", + modelId = "MARKER_MODEL_zXyq42", + promptColumn = "MARKER_PROMPT_zXyq42", + resultColumn = "MARKER_RESULT_zXyq42", + task = "MARKER_TASK_zXyq42", + systemPrompt = "MARKER_SYSTEM_zXyq42" + ), + StubCodegen + ) + out should not include "MARKER_TOKEN_zXyq42" + out should not include "MARKER_MODEL_zXyq42" + out should not include "MARKER_PROMPT_zXyq42" + out should not include "MARKER_RESULT_zXyq42" + out should not include "MARKER_TASK_zXyq42" + out should not include "MARKER_SYSTEM_zXyq42" + } +}
