diff --git a/CHANGELOG.md b/CHANGELOG.md index 41e41ee055..240b10942b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ releases may include breaking changes. ### Added +- ✨ Add binary-safe QDMI program submission and retrieval to FoMaC ([#1957]) + ([**@burgholzer**]) - ✨ Add versioned, relocatable configuration and stable-ID registration for QDMI device libraries, including disabled-ID reservations, fresh device sessions, idempotent registration, and external-device target metadata @@ -667,6 +669,7 @@ changelogs._ +[#1957]: https://github.com/munich-quantum-toolkit/core/pull/1957 [#1953]: https://github.com/munich-quantum-toolkit/core/pull/1953 [#1952]: https://github.com/munich-quantum-toolkit/core/pull/1952 [#1950]: https://github.com/munich-quantum-toolkit/core/pull/1950 diff --git a/bindings/fomac/fomac.cpp b/bindings/fomac/fomac.cpp index 00b93043c7..3606920292 100644 --- a/bindings/fomac/fomac.cpp +++ b/bindings/fomac/fomac.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -268,6 +269,14 @@ when the custom slot is unsupported.)pb"); job.def_prop_ro("program", &fomac::Job::getProgram, "The submitted program."); + job.def_prop_ro( + "program_bytes", + [](const fomac::Job& self) { + const auto program = self.getProgramBytes(); + return nb::bytes(program.data(), program.size()); + }, + "The exact bytes of the submitted program."); + job.def_prop_ro("num_shots", &fomac::Job::getNumShots, "The number of shots."); @@ -298,6 +307,7 @@ when the custom slot is unsupported.)pb"); .value("CALIBRATION", QDMI_PROGRAM_FORMAT_CALIBRATION) .value("QPY", QDMI_PROGRAM_FORMAT_QPY) .value("IQM_JSON", QDMI_PROGRAM_FORMAT_IQMJSON) + .value("BATCH_JOB", QDMI_PROGRAM_FORMAT_BATCHJOB) .value("CUSTOM1", QDMI_PROGRAM_FORMAT_CUSTOM1) .value("CUSTOM2", QDMI_PROGRAM_FORMAT_CUSTOM2) .value("CUSTOM3", QDMI_PROGRAM_FORMAT_CUSTOM3) @@ -407,12 +417,43 @@ The caller must provide the type documented by the device implementation. Use ``bytes`` to retrieve the value without interpretation. Returns ``None`` when the custom slot is unsupported.)pb"); - device.def("submit_job", &fomac::Device::submitJob, "program"_a, - "program_format"_a, "num_shots"_a, nb::kw_only(), - "custom1"_a = nb::none(), "custom2"_a = nb::none(), - "custom3"_a = nb::none(), "custom4"_a = nb::none(), - "custom5"_a = nb::none(), nb::rv_policy::reference_internal, - "Submits a job to the device."); + device.def( + "submit_job", + [](const fomac::Device& self, const std::string& program, + const QDMI_Program_Format format, const size_t numShots, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5) { + return self.submitJob(program, format, numShots, custom1, custom2, + custom3, custom4, custom5); + }, + "program"_a, "program_format"_a, "num_shots"_a, nb::kw_only(), + "custom1"_a = nb::none(), "custom2"_a = nb::none(), + "custom3"_a = nb::none(), "custom4"_a = nb::none(), + "custom5"_a = nb::none(), nb::rv_policy::reference_internal, + "Submits a text job to the device."); + + device.def( + "submit_job", + [](const fomac::Device& self, const nb::bytes& program, + const QDMI_Program_Format format, const size_t numShots, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5) { + const auto bytes = std::span{ + static_cast(program.data()), program.size()}; + return self.submitJob(bytes, format, numShots, custom1, custom2, + custom3, custom4, custom5); + }, + "program"_a, "program_format"_a, "num_shots"_a, nb::kw_only(), + "custom1"_a = nb::none(), "custom2"_a = nb::none(), + "custom3"_a = nb::none(), "custom4"_a = nb::none(), + "custom5"_a = nb::none(), nb::rv_policy::reference_internal, + "Submits an exact byte payload to the device."); device.def("__repr__", [](const fomac::Device& dev) { return ""; diff --git a/cmake/ExternalDependencies.cmake b/cmake/ExternalDependencies.cmake index 7e33612ee3..5ece29a7d9 100644 --- a/cmake/ExternalDependencies.cmake +++ b/cmake/ExternalDependencies.cmake @@ -99,24 +99,28 @@ FetchContent_Declare( FIND_PACKAGE_ARGS ${QDMI_MINIMUM_VERSION}) list(APPEND FETCH_PACKAGES qdmi) -set(SPDLOG_VERSION - 1.17.0 - CACHE STRING "spdlog version") -set(SPDLOG_URL https://github.com/gabime/spdlog/archive/refs/tags/v${SPDLOG_VERSION}.tar.gz) -# Add position independent code for spdlog, this is required for python bindings on linux -set(SPDLOG_BUILD_PIC ON) -set(SPDLOG_SYSTEM_INCLUDES - ON - CACHE INTERNAL "Treat the library headers like system headers") -cmake_dependent_option(MQT_CORE_SPDLOG_INSTALL "Install spdlog library" ON "MQT_CORE_INSTALL" OFF) -# Disable upstream spdlog install rules and install with explicit MQT components below. -set(SPDLOG_INSTALL - OFF - CACHE BOOL "Disable upstream spdlog install rules; handled by mqt-core" FORCE) -cmake_dependent_option(SPDLOG_BUILD_SHARED "Build spdlog as shared library" ON - "BUILD_MQT_CORE_SHARED_LIBS" OFF) -FetchContent_Declare(spdlog URL ${SPDLOG_URL} FIND_PACKAGE_ARGS ${SPDLOG_VERSION}) -list(APPEND FETCH_PACKAGES spdlog) +set(MQT_CORE_MANAGES_SPDLOG OFF) +if(NOT TARGET spdlog::spdlog) + set(SPDLOG_VERSION + 1.17.0 + CACHE STRING "spdlog version") + set(SPDLOG_URL https://github.com/gabime/spdlog/archive/refs/tags/v${SPDLOG_VERSION}.tar.gz) + # Add position independent code for spdlog, this is required for Python bindings on Linux. + set(SPDLOG_BUILD_PIC ON) + set(SPDLOG_SYSTEM_INCLUDES + ON + CACHE INTERNAL "Treat the library headers like system headers") + cmake_dependent_option(MQT_CORE_SPDLOG_INSTALL "Install spdlog library" ON "MQT_CORE_INSTALL" OFF) + # Disable upstream spdlog install rules and install with explicit MQT components below. + set(SPDLOG_INSTALL + OFF + CACHE BOOL "Disable upstream spdlog install rules; handled by mqt-core" FORCE) + cmake_dependent_option(SPDLOG_BUILD_SHARED "Build spdlog as shared library" ON + "BUILD_MQT_CORE_SHARED_LIBS" OFF) + FetchContent_Declare(spdlog URL ${SPDLOG_URL} FIND_PACKAGE_ARGS ${SPDLOG_VERSION}) + list(APPEND FETCH_PACKAGES spdlog) + set(MQT_CORE_MANAGES_SPDLOG ON) +endif() # Make all declared dependencies available. FetchContent_MakeAvailable(${FETCH_PACKAGES}) @@ -180,7 +184,7 @@ if(MQT_CORE_JSON_INSTALL AND TARGET nlohmann_json) endif() # Ensure external shared libraries end up in a common lib layout used by our RUNPATH -if(TARGET spdlog) +if(MQT_CORE_MANAGES_SPDLOG AND TARGET spdlog) set_target_properties( spdlog PROPERTIES LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}" @@ -189,7 +193,8 @@ if(TARGET spdlog) endif() # Install spdlog with explicit MQT components. -if(MQT_CORE_SPDLOG_INSTALL +if(MQT_CORE_MANAGES_SPDLOG + AND MQT_CORE_SPDLOG_INSTALL AND TARGET spdlog AND TARGET spdlog_header_only) include(CMakePackageConfigHelpers) diff --git a/docs/qir/index.md b/docs/qir/index.md index c105b2e67b..d10eca9c5d 100644 --- a/docs/qir/index.md +++ b/docs/qir/index.md @@ -57,3 +57,25 @@ Ordered; anything else, or a missing attribute, selects Labeled. The QDMI Device accepts jobs in the following program formats: QASM2, QASM3, QIR Base/Adaptive Profile Module (LLVM bitcode), and QIR Base/Adaptive Profile String (LLVM assembly). + +FoMaC C++ applications submit textual programs through the +`Device::submitJob(const std::string&, ...)` overload, which includes the +terminating null byte required by QDMI. Binary module payloads use the +`Device::submitJob(std::span, ...)` overload instead. It +preserves embedded null bytes and submits exactly the span's size without +appending a terminator. `Job::getProgramBytes()` retrieves such a payload +without interpreting its format or removing terminal null bytes; the existing +`Job::getProgram()` remains the textual, null-terminated accessor. It rejects +known binary and non-text formats based on their QDMI format identifier, even if +their payload happens to end in a null byte. + +The Python API follows the same distinction: pass `str` to `Device.submit_job` +for a textual program and `bytes` for an exact binary payload. +`Job.program_bytes` always returns the unmodified payload, while `Job.program` +expects a null-terminated UTF-8 text payload and rejects known binary or +non-text formats. + +The generic submission APIs intentionally reject QDMI calibration and batch-job +formats. Calibration jobs do not carry a program, while batch jobs contain job +handles rather than serialized program bytes. Their format identifiers remain +available for capability discovery; they require dedicated typed APIs. diff --git a/include/mqt-core/fomac/FoMaC.hpp b/include/mqt-core/fomac/FoMaC.hpp index 9aa6713410..652fffca3e 100644 --- a/include/mqt-core/fomac/FoMaC.hpp +++ b/include/mqt-core/fomac/FoMaC.hpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -517,7 +518,14 @@ class Device { std::to_string(static_cast(property))); } - /// @see QDMI_job_submit + /** + * @brief Submits a textual program. + * @details The terminating null byte required by QDMI text formats is + * included in the submitted payload. + * @throws std::invalid_argument If the format requires binary submission or + * does not carry a generic program payload. + * @see QDMI_job_submit + */ [[nodiscard]] Job submitJob( const std::string& program, QDMI_Program_Format format, size_t numShots, const std::optional& custom1 = std::nullopt, @@ -526,6 +534,23 @@ class Device { const std::optional& custom4 = std::nullopt, const std::optional& custom5 = std::nullopt) const; + /** + * @brief Submits a binary program. + * @details The bytes are submitted exactly as provided without appending a + * null byte. + * @throws std::invalid_argument If the format does not carry a generic + * program payload. + * @see QDMI_job_submit + */ + [[nodiscard]] Job submitJob( + std::span program, QDMI_Program_Format format, + size_t numShots, + const std::optional& custom1 = std::nullopt, + const std::optional& custom2 = std::nullopt, + const std::optional& custom3 = std::nullopt, + const std::optional& custom4 = std::nullopt, + const std::optional& custom5 = std::nullopt) const; + auto operator<=>(const Device&) const noexcept = default; private: @@ -649,9 +674,18 @@ class Job { /// Get the program format [[nodiscard]] QDMI_Program_Format getProgramFormat() const; - /// Get the program to be executed + /** + * @brief Gets a textual program without its terminating null byte. + * @throws std::invalid_argument If the format is not textual or the device + * does not return a null-terminated payload. + */ [[nodiscard]] std::string getProgram() const; + /** + * @brief Gets the submitted program bytes exactly as returned by the device. + */ + [[nodiscard]] std::vector getProgramBytes() const; + /// Get the number of shots [[nodiscard]] size_t getNumShots() const; diff --git a/python/mqt/core/fomac.pyi b/python/mqt/core/fomac.pyi index e6494a113d..0c3e77df02 100644 --- a/python/mqt/core/fomac.pyi +++ b/python/mqt/core/fomac.pyi @@ -172,6 +172,10 @@ class Job: def program(self) -> str: """The submitted program.""" + @property + def program_bytes(self) -> bytes: + """The exact bytes of the submitted program.""" + @property def num_shots(self) -> int: """The number of shots.""" @@ -217,6 +221,8 @@ class ProgramFormat(enum.Enum): IQM_JSON = 8 + BATCH_JOB = 9 + CUSTOM1 = 999999995 CUSTOM2 = 999999996 @@ -333,6 +339,7 @@ class Device: when the custom slot is unsupported. """ + @overload def submit_job( self, program: str, @@ -345,7 +352,22 @@ class Device: custom4: str | bool | float | None = None, custom5: str | bool | float | None = None, ) -> Job: - """Submits a job to the device.""" + """Submits a text job to the device.""" + + @overload + def submit_job( + self, + program: bytes, + program_format: ProgramFormat, + num_shots: int, + *, + custom1: str | bool | float | None = None, + custom2: str | bool | float | None = None, + custom3: str | bool | float | None = None, + custom4: str | bool | float | None = None, + custom5: str | bool | float | None = None, + ) -> Job: + """Submits an exact byte payload to the device.""" def __eq__(self, arg: object, /) -> bool: ... def __ne__(self, arg: object, /) -> bool: ... diff --git a/src/fomac/FoMaC.cpp b/src/fomac/FoMaC.cpp index 06e9ad7d55..fb3e7156a6 100644 --- a/src/fomac/FoMaC.cpp +++ b/src/fomac/FoMaC.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,21 @@ #include namespace fomac { +namespace { +[[nodiscard]] constexpr bool +isBinaryProgramFormat(const QDMI_Program_Format format) noexcept { + return format == QDMI_PROGRAM_FORMAT_QIRBASEMODULE || + format == QDMI_PROGRAM_FORMAT_QIRADAPTIVEMODULE || + format == QDMI_PROGRAM_FORMAT_QPY; +} + +[[nodiscard]] constexpr bool +hasNoGenericProgramPayload(const QDMI_Program_Format format) noexcept { + return format == QDMI_PROGRAM_FORMAT_CALIBRATION || + format == QDMI_PROGRAM_FORMAT_BATCHJOB; +} +} // namespace + size_t Site::getIndex() const { return queryProperty(QDMI_SITE_PROPERTY_INDEX); } @@ -326,6 +342,33 @@ Job Device::submitJob(const std::string& program, const std::optional& custom3, const std::optional& custom4, const std::optional& custom5) const { + if (isBinaryProgramFormat(format)) { + throw std::invalid_argument( + "Binary program formats require exact-byte submission"); + } + if (hasNoGenericProgramPayload(format)) { + throw std::invalid_argument( + "Calibration and batch jobs do not use a generic program payload"); + } + + const auto bytes = std::as_bytes( + std::span(program.c_str(), static_cast(program.size() + 1))); + return submitJob(bytes, format, numShots, custom1, custom2, custom3, custom4, + custom5); +} + +Job Device::submitJob(const std::span program, + const QDMI_Program_Format format, const size_t numShots, + const std::optional& custom1, + const std::optional& custom2, + const std::optional& custom3, + const std::optional& custom4, + const std::optional& custom5) const { + if (hasNoGenericProgramPayload(format)) { + throw std::invalid_argument( + "Calibration and batch jobs do not use a generic program payload"); + } + QDMI_Job job = nullptr; qdmi::throwIfError(QDMI_device_create_job(device_.get(), &job), "Creating job"); @@ -335,10 +378,10 @@ Job Device::submitJob(const std::string& program, QDMI_JOB_PARAMETER_PROGRAMFORMAT, sizeof(format), &format), "Setting program format"); - qdmi::throwIfError( - QDMI_job_set_parameter(jobWrapper, QDMI_JOB_PARAMETER_PROGRAM, - program.size() + 1, program.c_str()), - "Setting program"); + qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, + QDMI_JOB_PARAMETER_PROGRAM, + program.size(), program.data()), + "Setting program"); qdmi::throwIfError(QDMI_job_set_parameter(jobWrapper, QDMI_JOB_PARAMETER_SHOTSNUM, sizeof(numShots), &numShots), @@ -439,21 +482,39 @@ QDMI_Program_Format Job::getProgramFormat() const { return format; } -std::string Job::getProgram() const { +std::vector Job::getProgramBytes() const { size_t size = 0; qdmi::throwIfError(QDMI_job_query_property(job_.get(), QDMI_JOB_PROPERTY_PROGRAM, 0, nullptr, &size), "Querying program size"); - std::string program(size - 1, '\0'); - qdmi::throwIfError(QDMI_job_query_property(job_.get(), - QDMI_JOB_PROPERTY_PROGRAM, size, - program.data(), nullptr), - "Querying program"); + std::vector program(size); + if (size != 0) { + qdmi::throwIfError(QDMI_job_query_property(job_.get(), + QDMI_JOB_PROPERTY_PROGRAM, size, + program.data(), nullptr), + "Querying program"); + } return program; } +std::string Job::getProgram() const { + const auto format = getProgramFormat(); + if (isBinaryProgramFormat(format)) { + throw std::invalid_argument( + "Cannot decode a binary program as a string; use getProgramBytes()"); + } + + const auto program = getProgramBytes(); + if (program.empty() || program.back() != std::byte{0}) { + throw std::invalid_argument( + "Cannot decode program as a null-terminated string; use " + "getProgramBytes() for binary payloads"); + } + return {reinterpret_cast(program.data()), program.size() - 1}; +} + size_t Job::getNumShots() const { size_t numShots = 0; qdmi::throwIfError( diff --git a/src/qdmi/devices/dd/Device.cpp b/src/qdmi/devices/dd/Device.cpp index 2653bb9543..7a85a8038c 100644 --- a/src/qdmi/devices/dd/Device.cpp +++ b/src/qdmi/devices/dd/Device.cpp @@ -383,8 +383,15 @@ auto MQT_DDSIM_QDMI_Device_Job_impl_d::setParameter( if (isTextProgramFormat) { // Text payloads include the trailing '\0' in `size`. // Strip it so it is not counted in the stored string's size. - const auto* text = static_cast(value); - program_ = std::string(text, size - 1); + const std::span text{static_cast(value), size}; + if (text.empty() || text.back() != '\0') { + return QDMI_ERROR_INVALIDARGUMENT; + } + const auto contents = text.first(text.size() - 1); + if (std::ranges::find(contents, '\0') != contents.end()) { + return QDMI_ERROR_INVALIDARGUMENT; + } + program_ = std::string(contents.begin(), contents.end()); } else { // Binary payloads are stored exactly as received. const std::span bytes(static_cast(value), size); diff --git a/test/fomac/test_fomac.cpp b/test/fomac/test_fomac.cpp index 8182e89d35..07d16ef437 100644 --- a/test/fomac/test_fomac.cpp +++ b/test/fomac/test_fomac.cpp @@ -739,6 +739,21 @@ c = measure q;)"; EXPECT_EQ(job.check(), QDMI_JOB_STATUS_DONE); } +TEST_F(DDSimulatorDeviceTest, SubmitJobRejectsIncompatiblePayloadKinds) { + const std::string textProgram = "OPENQASM 3.0;"; + constexpr std::array bytes{std::byte{0}}; + + EXPECT_THROW(std::ignore = device.submitJob( + textProgram, QDMI_PROGRAM_FORMAT_QIRBASEMODULE, 0), + std::invalid_argument); + EXPECT_THROW(std::ignore = device.submitJob( + textProgram, QDMI_PROGRAM_FORMAT_CALIBRATION, 0), + std::invalid_argument); + EXPECT_THROW(std::ignore = + device.submitJob(bytes, QDMI_PROGRAM_FORMAT_BATCHJOB, 0), + std::invalid_argument); +} + TEST_F(DDSimulatorDeviceTest, SubmitJobCustomSupportedTypes) { constexpr auto qasm3Program = "OPENQASM 3.0;"; diff --git a/test/python/fomac/test_fomac.py b/test/python/fomac/test_fomac.py index eee4979b9f..4604c1e67c 100644 --- a/test/python/fomac/test_fomac.py +++ b/test/python/fomac/test_fomac.py @@ -471,10 +471,36 @@ def test_device_submit_job_returns_valid_job(ddsim_device: Device) -> None: assert job.program_format == ProgramFormat.QASM3 # The program should be preserved assert job.program == qasm3_program + assert job.program_bytes == qasm3_program.encode() + b"\0" # Num shots should match request assert job.num_shots == 100 +def test_program_format_includes_batch_job() -> None: + """Expose every standard QDMI program format.""" + assert ProgramFormat.BATCH_JOB.value == 9 + + +@pytest.mark.parametrize("program", [b"OPENQASM 3.0;", b"OPENQASM 3.0;\0garbage\0", "OPENQASM 3.0;\0garbage"]) +def test_device_rejects_invalid_text_payloads(ddsim_device: Device, program: str | bytes) -> None: + """Reject payloads that do not satisfy QDMI's text contract.""" + with pytest.raises(ValueError, match=r"Setting program: Invalid argument\."): + ddsim_device.submit_job(program, ProgramFormat.QASM3, num_shots=1) + + +def test_device_rejects_text_for_binary_format(ddsim_device: Device) -> None: + """Require exact byte submission for known binary formats.""" + with pytest.raises(ValueError, match="require exact-byte submission"): + ddsim_device.submit_job("not bitcode", ProgramFormat.QIR_BASE_MODULE, num_shots=1) + + +@pytest.mark.parametrize("program_format", [ProgramFormat.CALIBRATION, ProgramFormat.BATCH_JOB]) +def test_device_rejects_formats_without_generic_payload(ddsim_device: Device, program_format: ProgramFormat) -> None: + """Keep specialized QDMI formats out of the generic program API.""" + with pytest.raises(ValueError, match="do not use a generic program payload"): + ddsim_device.submit_job(b"", program_format, num_shots=1) + + def test_device_executes_qir_program(ddsim_device: Device) -> None: """Compile and execute a QIR program with the DDSIM device.""" # Keep this lazy to cover loading MLIR after the QIR-enabled device. @@ -499,6 +525,33 @@ def test_device_executes_qir_program(ddsim_device: Device) -> None: assert sum(job.get_counts().values()) == 10 +def test_device_executes_binary_qir_program(ddsim_device: Device) -> None: + """Submit and retrieve an exact QIR module byte payload.""" + from mqt.core.mlir import OutputFormat, compile_program # ruff:ignore[import-outside-top-level] + + qasm3_program = """ +OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] q; +bit[2] c; +h q[0]; +cx q[0], q[1]; +c = measure q; +""" + program = compile_program(qasm3_program, output=OutputFormat.QIR_BASE) + program_bytes = program.to_bitcode() + assert ProgramFormat.QIR_BASE_MODULE in ddsim_device.supported_program_formats() + + job = ddsim_device.submit_job(program_bytes, ProgramFormat.QIR_BASE_MODULE, num_shots=10) + assert job.program_bytes == program_bytes + with pytest.raises(ValueError, match="binary program"): + _ = job.program + job.wait() + + assert job.check() == Job.Status.DONE + assert sum(job.get_counts().values()) == 10 + + def test_device_submit_job_handles_custom_parameters(ddsim_device: Device) -> None: """Test that submit_job forwards custom job parameters to DDSIM.""" with pytest.raises(RuntimeError, match=r"Setting custom parameter: Not supported\."): diff --git a/test/qdmi/devices/dd/job_parameters_test.cpp b/test/qdmi/devices/dd/job_parameters_test.cpp index eddfcb58ce..9d62a9b9ba 100644 --- a/test/qdmi/devices/dd/job_parameters_test.cpp +++ b/test/qdmi/devices/dd/job_parameters_test.cpp @@ -18,6 +18,7 @@ #include +#include #include #include #include @@ -85,6 +86,40 @@ TEST(JobParameters, SetAndQueryBasics) { EXPECT_EQ(program, qdmi_test::QASM3_BELL_SAMPLING); } +TEST(JobParameters, RejectsUnterminatedTextProgram) { + const qdmi_test::SessionGuard s{}; + const qdmi_test::JobGuard j{s.session}; + + constexpr QDMI_Program_Format fmt = QDMI_PROGRAM_FORMAT_QASM3; + ASSERT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( + j.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT, + sizeof(QDMI_Program_Format), &fmt), + QDMI_SUCCESS); + + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( + j.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAM, + strlen(qdmi_test::QASM3_BELL_SAMPLING), + qdmi_test::QASM3_BELL_SAMPLING), + QDMI_ERROR_INVALIDARGUMENT); +} + +TEST(JobParameters, RejectsInteriorNullInTextProgram) { + const qdmi_test::SessionGuard s{}; + const qdmi_test::JobGuard j{s.session}; + + constexpr QDMI_Program_Format fmt = QDMI_PROGRAM_FORMAT_QASM3; + ASSERT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( + j.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAMFORMAT, + sizeof(QDMI_Program_Format), &fmt), + QDMI_SUCCESS); + + constexpr auto program = std::to_array("OPENQASM 3.0;\0garbage"); + EXPECT_EQ(MQT_DDSIM_QDMI_device_job_set_parameter( + j.job, QDMI_DEVICE_JOB_PARAMETER_PROGRAM, program.size(), + program.data()), + QDMI_ERROR_INVALIDARGUMENT); +} + TEST(JobParameters, ProgramFormatSupport) { const qdmi_test::SessionGuard s{}; const qdmi_test::JobGuard j{s.session};