# Copyright (c) ONNX Project Contributors

# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations

import contextlib
import os
import struct
import tempfile
import unittest

import numpy as np
import parameterized

import onnx.version_converter
from onnx import (
    GraphProto,
    ModelProto,
    OperatorSetIdProto,
    TensorProto,
    checker,
    helper,
    shape_inference,
)


class TestVersionConverter(unittest.TestCase):
    def _converted(
        self,
        graph: GraphProto,
        initial_version: OperatorSetIdProto,
        target_version: int,
    ) -> ModelProto:
        orig_model = helper.make_model(
            graph, producer_name="onnx-test", opset_imports=[initial_version]
        )
        # print(type(orig_model))
        converted_model = onnx.version_converter.convert_version(
            orig_model, target_version
        )
        checker.check_model(converted_model)
        return converted_model

    # Test 1: Backwards Incompatible Conversion: Reshape: 8 -> 2
    def test_backwards_incompatible(self) -> None:
        def test() -> None:
            nodes = [
                helper.make_node("Add", ["W", "Z"], ["shape"]),
                helper.make_node("Reshape", ["X", "shape"], ["A"]),
                helper.make_node("Add", ["A", "W"], ["Y"]),
            ]
            graph = helper.make_graph(
                nodes,
                "test",
                [
                    helper.make_tensor_value_info("X", TensorProto.FLOAT, (5,)),
                    helper.make_tensor_value_info("W", TensorProto.FLOAT, (1,)),
                    helper.make_tensor_value_info("Z", TensorProto.FLOAT, (1,)),
                ],
                [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5,))],
            )
            self._converted(graph, helper.make_operatorsetid("", 8), 2)

        self.assertRaises(RuntimeError, test)

    # Test 2: Backwards Compatible Conversion (No Adaptations): Add: 3 -> 2
    def test_backwards_compatible(self) -> None:
        nodes = [helper.make_node("Add", ["X1", "X2"], ["Y"])]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info("X1", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("X2", TensorProto.FLOAT, (5,)),
            ],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 3), 2)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Add"
        assert converted_model.opset_import[0].version == 2

    # Test 3: Non-Existent Op Conversion: Cos: 8 -> 6
    def test_non_existent_op(self) -> None:
        def test() -> None:
            nodes = [helper.make_node("Cos", ["X"], ["Y"])]
            graph = helper.make_graph(
                nodes,
                "test",
                [helper.make_tensor_value_info("X", TensorProto.FLOAT, (5,))],
                [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5,))],
            )
            self._converted(graph, helper.make_operatorsetid("", 8), 6)

        self.assertRaises(RuntimeError, test)

    # Test Add Adapter: 8 -> 5
    def test_add_8_5(self) -> None:
        nodes = [helper.make_node("Add", ["X1", "X2"], ["Y"])]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info("X1", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("X2", TensorProto.FLOAT, (1,)),
            ],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 8), 5)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Add"
        assert converted_model.opset_import[0].version == 5

    # Test Add Adapter: 5 -> 8
    def test_add_5_8(self) -> None:
        nodes = [helper.make_node("Add", ["X1", "X2"], ["Y"])]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info("X1", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("X2", TensorProto.FLOAT, (1,)),
            ],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 5), 8)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Add"
        assert converted_model.opset_import[0].version == 8

    # Test Add Adapter: 5 -> 8, requiring insertion of an Unsqueeze node
    def test_add_5_8_with_unsqueeze(self) -> None:
        nodes = [helper.make_node("Add", ["X1", "X2"], ["Y"], axis=0, broadcast=1)]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info("X1", TensorProto.FLOAT, (5, 2)),
                helper.make_tensor_value_info("X2", TensorProto.FLOAT, (5,)),
            ],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 5), 8)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Unsqueeze"
        assert converted_model.graph.node[1].op_type == "Add"
        assert converted_model.opset_import[0].version == 8

    # Test Mul Adapter: 8 -> 5
    def test_mul_8_5(self) -> None:
        nodes = [helper.make_node("Mul", ["X1", "X2"], ["Y"])]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info("X1", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("X2", TensorProto.FLOAT, (1,)),
            ],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 8), 5)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Mul"
        assert converted_model.opset_import[0].version == 5

    # Test Mul Adapter: 5 -> 8
    def test_mul_5_8(self) -> None:
        nodes = [helper.make_node("Mul", ["X1", "X2"], ["Y"])]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info("X1", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("X2", TensorProto.FLOAT, (1,)),
            ],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 5), 8)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Mul"
        assert converted_model.opset_import[0].version == 8

    # Test Gemm Adapter: 1 -> 8
    def test_gemm_up(self) -> None:
        nodes = [helper.make_node("Gemm", ["A", "B", "C"], ["Y"])]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info(
                    "A",
                    TensorProto.FLOAT,
                    (
                        5,
                        5,
                    ),
                ),
                helper.make_tensor_value_info(
                    "B",
                    TensorProto.FLOAT,
                    (
                        5,
                        5,
                    ),
                ),
                helper.make_tensor_value_info(
                    "C",
                    TensorProto.FLOAT,
                    (
                        5,
                        5,
                    ),
                ),
            ],
            [
                helper.make_tensor_value_info(
                    "Y",
                    TensorProto.FLOAT,
                    (
                        5,
                        5,
                    ),
                )
            ],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 1), 8)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Gemm"
        assert converted_model.opset_import[0].version == 8

    # Test Gemm Adapter: 8 -> 1
    def test_gemm_down(self) -> None:
        nodes = [helper.make_node("Gemm", ["A", "B", "C"], ["Y"])]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info(
                    "A",
                    TensorProto.FLOAT,
                    (
                        5,
                        5,
                    ),
                ),
                helper.make_tensor_value_info(
                    "B",
                    TensorProto.FLOAT,
                    (
                        5,
                        5,
                    ),
                ),
                helper.make_tensor_value_info(
                    "C",
                    TensorProto.FLOAT,
                    (
                        5,
                        5,
                    ),
                ),
            ],
            [
                helper.make_tensor_value_info(
                    "Y",
                    TensorProto.FLOAT,
                    (
                        5,
                        5,
                    ),
                )
            ],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 8), 1)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Gemm"
        assert converted_model.opset_import[0].version == 1

    def test_gemm_7_6_rejects_1d_input(self) -> None:
        # Regression test: heap-buffer-overflow when B has rank < 2 (advisory)
        def test() -> None:
            nodes = [helper.make_node("Gemm", ["A", "B", "C"], ["Y"])]
            graph = helper.make_graph(
                nodes,
                "test_gemm_7_6_rejects_1d_input",
                [
                    helper.make_tensor_value_info("A", TensorProto.FLOAT, (4, 3)),
                    helper.make_tensor_value_info("B", TensorProto.FLOAT, (28,)),
                    helper.make_tensor_value_info("C", TensorProto.FLOAT, (4,)),
                ],
                [helper.make_tensor_value_info("Y", TensorProto.FLOAT, None)],
            )
            self._converted(graph, helper.make_operatorsetid("", 7), 6)

        self.assertRaises(RuntimeError, test)

    def test_gemm_6_7_rejects_1d_input(self) -> None:
        # Regression test: heap-buffer-overflow when B has rank < 2 (same pattern, upward)
        def test() -> None:
            nodes = [helper.make_node("Gemm", ["A", "B", "C"], ["Y"])]
            graph = helper.make_graph(
                nodes,
                "test_gemm_6_7_rejects_1d_input",
                [
                    helper.make_tensor_value_info("A", TensorProto.FLOAT, (4, 3)),
                    helper.make_tensor_value_info("B", TensorProto.FLOAT, (28,)),
                    helper.make_tensor_value_info("C", TensorProto.FLOAT, (4,)),
                ],
                [helper.make_tensor_value_info("Y", TensorProto.FLOAT, None)],
            )
            self._converted(graph, helper.make_operatorsetid("", 6), 7)

        self.assertRaises(RuntimeError, test)

    def test_gemm_7_6_rejects_1d_A(self) -> None:
        # Exercises the A-rank guard (B is valid rank-2, A is rank-1).
        def test() -> None:
            nodes = [helper.make_node("Gemm", ["A", "B", "C"], ["Y"])]
            graph = helper.make_graph(
                nodes,
                "test_gemm_7_6_rejects_1d_A",
                [
                    helper.make_tensor_value_info("A", TensorProto.FLOAT, (12,)),
                    helper.make_tensor_value_info("B", TensorProto.FLOAT, (3, 4)),
                    helper.make_tensor_value_info("C", TensorProto.FLOAT, (4,)),
                ],
                [helper.make_tensor_value_info("Y", TensorProto.FLOAT, None)],
            )
            self._converted(graph, helper.make_operatorsetid("", 7), 6)

        self.assertRaises(RuntimeError, test)

    def test_gemm_6_7_rejects_1d_A(self) -> None:
        # Exercises the A-rank guard (B is valid rank-2, A is rank-1).
        def test() -> None:
            nodes = [helper.make_node("Gemm", ["A", "B", "C"], ["Y"])]
            graph = helper.make_graph(
                nodes,
                "test_gemm_6_7_rejects_1d_A",
                [
                    helper.make_tensor_value_info("A", TensorProto.FLOAT, (12,)),
                    helper.make_tensor_value_info("B", TensorProto.FLOAT, (3, 4)),
                    helper.make_tensor_value_info("C", TensorProto.FLOAT, (4,)),
                ],
                [helper.make_tensor_value_info("Y", TensorProto.FLOAT, None)],
            )
            self._converted(graph, helper.make_operatorsetid("", 6), 7)

        self.assertRaises(RuntimeError, test)

    # Test Relu Adapter: 5 -> 7
    def test_relu_5_7(self) -> None:
        nodes = [helper.make_node("Relu", ["X"], ["Y"])]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (5,))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 5), 7)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Relu"
        assert converted_model.opset_import[0].version == 7

    # Test Relu Adapter: 7 -> 5
    def test_relu_7_5(self) -> None:
        nodes = [helper.make_node("Relu", ["X"], ["Y"])]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (5,))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 7), 5)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Relu"
        assert converted_model.opset_import[0].version == 5

    # Test BatchNormalization Adapter: 8 -> 5
    def test_batch_normalization_8_5(self) -> None:
        nodes = [
            helper.make_node(
                "BatchNormalization", ["X", "scale", "B", "mean", "var"], ["Y"]
            )
        ]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info("X", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("scale", TensorProto.FLOAT, (1,)),
                helper.make_tensor_value_info("B", TensorProto.FLOAT, (1,)),
                helper.make_tensor_value_info("mean", TensorProto.FLOAT, (1,)),
                helper.make_tensor_value_info("var", TensorProto.FLOAT, (1,)),
            ],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 8), 5)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "BatchNormalization"
        assert converted_model.opset_import[0].version == 5

    # Test BatchNormalization Adapter: 5 -> 8
    def test_batch_normalization_5_8(self) -> None:
        nodes = [
            helper.make_node(
                "BatchNormalization", ["X", "scale", "B", "mean", "var"], ["Y"]
            )
        ]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info("X", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("scale", TensorProto.FLOAT, (1,)),
                helper.make_tensor_value_info("B", TensorProto.FLOAT, (1,)),
                helper.make_tensor_value_info("mean", TensorProto.FLOAT, (1,)),
                helper.make_tensor_value_info("var", TensorProto.FLOAT, (1,)),
            ],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 5), 8)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "BatchNormalization"
        assert converted_model.opset_import[0].version == 8

    # Test Concat Adapter: 3 -> 5
    def test_concat_3_5(self) -> None:
        nodes = [helper.make_node("Concat", ["X1", "X2", "X3", "X4", "X5"], ["Y"])]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info("X1", TensorProto.FLOAT, (1,)),
                helper.make_tensor_value_info("X2", TensorProto.FLOAT, (1,)),
                helper.make_tensor_value_info("X3", TensorProto.FLOAT, (1,)),
                helper.make_tensor_value_info("X4", TensorProto.FLOAT, (1,)),
                helper.make_tensor_value_info("X5", TensorProto.FLOAT, (1,)),
            ],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 3), 5)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Concat"
        assert converted_model.opset_import[0].version == 5

    # Test Concat Adapter: 5 -> 3
    def test_concat_5_3(self) -> None:
        nodes = [
            helper.make_node("Concat", ["X1", "X2", "X3", "X4", "X5"], ["Y"], axis=0)
        ]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info("X1", TensorProto.FLOAT, (1,)),
                helper.make_tensor_value_info("X2", TensorProto.FLOAT, (1,)),
                helper.make_tensor_value_info("X3", TensorProto.FLOAT, (1,)),
                helper.make_tensor_value_info("X4", TensorProto.FLOAT, (1,)),
                helper.make_tensor_value_info("X5", TensorProto.FLOAT, (1,)),
            ],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 5), 3)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Concat"
        assert converted_model.opset_import[0].version == 3

    # Test Reshape Adapter: 6 -> 4
    def test_reshape_6_4(self) -> None:
        nodes = [
            helper.make_node(
                "Constant",
                [],
                ["shape"],
                value=helper.make_tensor("", TensorProto.INT64, [1], [5]),
            ),
            helper.make_node("Reshape", ["X", "shape"], ["Y"]),
        ]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (5,))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 6), 4)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Reshape"
        assert converted_model.opset_import[0].version == 4

    # Test Reshape Adapter: 4 -> 6
    def test_reshape_4_6(self) -> None:
        nodes = [helper.make_node("Reshape", ["X"], ["Y"], shape=[5])]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (5,))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 4), 6)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Constant"
        assert converted_model.graph.node[1].op_type == "Reshape"
        assert converted_model.opset_import[0].version == 6

    # Test Sum Adapter: 7 -> 8
    def test_sum_7_8(self) -> None:
        nodes = [
            helper.make_node(
                "Sum", ["data_0", "data_1", "data_2", "data_3", "data_4"], ["sum"]
            )
        ]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info("data_0", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("data_1", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("data_2", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("data_3", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("data_4", TensorProto.FLOAT, (5,)),
            ],
            [helper.make_tensor_value_info("sum", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 7), 8)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Sum"
        assert converted_model.opset_import[0].version == 8

    # Test Sum Adapter: 5 -> 8
    def test_sum_5_8(self) -> None:
        nodes = [
            helper.make_node(
                "Sum", ["data_0", "data_1", "data_2", "data_3", "data_4"], ["sum"]
            )
        ]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info("data_0", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("data_1", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("data_2", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("data_3", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("data_4", TensorProto.FLOAT, (5,)),
            ],
            [helper.make_tensor_value_info("sum", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 5), 7)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Sum"
        assert converted_model.opset_import[0].version == 7

    # Test Sum Adapter: 8 -> 5
    def test_sum_8_5(self) -> None:
        nodes = [
            helper.make_node(
                "Sum", ["data_0", "data_1", "data_2", "data_3", "data_4"], ["sum"]
            )
        ]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info("data_0", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("data_1", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("data_2", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("data_3", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("data_4", TensorProto.FLOAT, (5,)),
            ],
            [helper.make_tensor_value_info("sum", TensorProto.FLOAT, (5,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 8), 5)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Sum"
        assert converted_model.opset_import[0].version == 5

    # Test AveragePool Adapter: 1 -> 8
    def test_averagepool_up(self) -> None:
        nodes = [helper.make_node("AveragePool", ["X"], ["Y"], kernel_shape=[1, 1])]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (5, 5, 5, 5))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5, 5, 5, 5))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 1), 8)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "AveragePool"
        assert converted_model.opset_import[0].version == 8

    # Test AveragePool Adapter: 8 -> 1
    def test_averagepool_down(self) -> None:
        nodes = [helper.make_node("AveragePool", ["X"], ["Y"], kernel_shape=[1, 1])]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (5, 5, 5, 5))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5, 5, 5, 5))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 8), 1)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "AveragePool"
        assert converted_model.opset_import[0].version == 1

    # Test Dropout Adapter: 1 -> 8
    def test_dropout_up(self) -> None:
        nodes = [helper.make_node("Dropout", ["data"], ["output"], is_test=1)]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info(
                    "data",
                    TensorProto.FLOAT,
                    (
                        5,
                        5,
                    ),
                )
            ],
            [
                helper.make_tensor_value_info(
                    "output",
                    TensorProto.FLOAT,
                    (
                        5,
                        5,
                    ),
                )
            ],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 1), 8)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Dropout"
        assert converted_model.opset_import[0].version == 8

    # Test Dropout Adapter: 8 -> 1
    def test_dropout_down(self) -> None:
        nodes = [helper.make_node("Dropout", ["data"], ["output"])]
        graph = helper.make_graph(
            nodes,
            "test",
            [
                helper.make_tensor_value_info(
                    "data",
                    TensorProto.FLOAT,
                    (
                        5,
                        5,
                    ),
                )
            ],
            [
                helper.make_tensor_value_info(
                    "output",
                    TensorProto.FLOAT,
                    (
                        5,
                        5,
                    ),
                )
            ],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 8), 1)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Dropout"
        assert converted_model.opset_import[0].version == 1

    # Test Max Adapter: 7 -> 8
    def test_max_7_8(self) -> None:
        from_opset = 7
        to_opset = 8
        data_type = TensorProto.FLOAT
        data_shape = (2, 3, 4)

        nodes = [onnx.helper.make_node("Max", inputs=["X"], outputs=["Y"])]

        graph = helper.make_graph(
            nodes,
            "test_max",
            [onnx.helper.make_tensor_value_info("X", data_type, data_shape)],
            [onnx.helper.make_tensor_value_info("Y", data_type, data_shape)],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "Max"
        assert converted_model.graph.output[0].type.tensor_type.elem_type == data_type
        assert converted_model.opset_import[0].version == to_opset

    # Test Min Adapter: 7 -> 8
    def test_min_7_8(self) -> None:
        from_opset = 7
        to_opset = 8
        data_type = TensorProto.FLOAT
        data_shape = (2, 3, 4)

        nodes = [onnx.helper.make_node("Min", inputs=["X"], outputs=["Y"])]

        graph = helper.make_graph(
            nodes,
            "test_min",
            [onnx.helper.make_tensor_value_info("X", data_type, data_shape)],
            [onnx.helper.make_tensor_value_info("Y", data_type, data_shape)],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "Min"
        assert converted_model.graph.output[0].type.tensor_type.elem_type == data_type
        assert converted_model.opset_import[0].version == to_opset

    # Test Mean Adapter: 7 -> 8
    def test_mean_7_8(self) -> None:
        from_opset = 7
        to_opset = 8
        data_type = TensorProto.FLOAT
        data_shape = (3,)

        nodes = [onnx.helper.make_node("Mean", inputs=["X"], outputs=["Y"])]

        graph = helper.make_graph(
            nodes,
            "test_mean",
            [onnx.helper.make_tensor_value_info("X", data_type, data_shape)],
            [onnx.helper.make_tensor_value_info("Y", data_type, data_shape)],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "Mean"
        assert converted_model.graph.output[0].type.tensor_type.elem_type == data_type
        assert converted_model.opset_import[0].version == to_opset

    # Test MaxPool Adapter: 1 -> 8
    def test_maxpool_up(self) -> None:
        nodes = [helper.make_node("MaxPool", ["X"], ["Y"], kernel_shape=[1, 1])]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (5, 5, 5, 5))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5, 5, 5, 5))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 1), 8)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "MaxPool"
        assert converted_model.opset_import[0].version == 8

    # Test Upsample Adapter: 6 -> 7
    def test_upsample_6_7(self) -> None:
        from_opset = 6
        to_opset = 7
        data_type = TensorProto.FLOAT

        nodes = [
            onnx.helper.make_node(
                "Upsample",
                inputs=["X"],
                outputs=["Y"],
                mode="nearest",
                width_scale=3.0,
                height_scale=2.0,
            )
        ]

        graph = helper.make_graph(
            nodes,
            "test_upsample_6_7",
            [onnx.helper.make_tensor_value_info("X", data_type, [1, 1, 2, 2])],
            [onnx.helper.make_tensor_value_info("Y", data_type, [1, 1, 4, 6])],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert len(converted_model.graph.node) == 1
        assert converted_model.graph.node[0].op_type == "Upsample"
        attribute_names = [
            attr.name for attr in converted_model.graph.node[0].attribute
        ]
        assert "scales" in attribute_names
        assert "width_scale" not in attribute_names
        assert "height_scale" not in attribute_names
        assert converted_model.opset_import[0].version == to_opset

    # Test MaxPool Adapter: 8 -> 1
    def test_maxpool_down(self) -> None:
        nodes = [helper.make_node("MaxPool", ["X"], ["Y"], kernel_shape=[1, 1])]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (5, 5, 5, 5))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (5, 5, 5, 5))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 8), 1)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "MaxPool"
        assert converted_model.opset_import[0].version == 1

    # Test BatchNormalization Adapter: 8 -> 9
    def test_batch_normalization_8_9(self) -> None:
        from_opset = 8
        to_opset = 9
        data_type = TensorProto.FLOAT

        nodes = [
            helper.make_node(
                "BatchNormalization",
                inputs=["x", "s", "bias", "mean", "var"],
                outputs=["y"],
            )
        ]

        input_shape = (1, 2, 1, 3)
        x = helper.make_tensor_value_info("x", data_type, input_shape)
        scale = helper.make_tensor_value_info("s", data_type, [input_shape[1]])
        B = helper.make_tensor_value_info("bias", data_type, [input_shape[1]])
        mean = helper.make_tensor_value_info("mean", data_type, [input_shape[1]])
        var = helper.make_tensor_value_info("var", data_type, [input_shape[1]])
        y = helper.make_tensor_value_info("y", data_type, input_shape)

        graph = helper.make_graph(
            nodes, "test_batchnormalization_8_9", [x, scale, B, mean, var], [y]
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "BatchNormalization"
        assert converted_model.opset_import[0].version == to_opset

    # Test BatchNormalization Adapter: 9 -> 8
    def test_batchnormalization_9_8(self) -> None:
        from_opset = 9
        to_opset = 8
        data_type = TensorProto.FLOAT

        nodes = [
            onnx.helper.make_node(
                "BatchNormalization",
                inputs=["X", "scale", "B", "mean", "var"],
                outputs=["Y"],
            )
        ]

        input_shape = (2, 3, 4, 5)
        x = onnx.helper.make_tensor_value_info("X", data_type, input_shape)
        scale = onnx.helper.make_tensor_value_info("scale", data_type, [input_shape[1]])
        B = onnx.helper.make_tensor_value_info("B", data_type, [input_shape[1]])
        mean = onnx.helper.make_tensor_value_info("mean", data_type, [input_shape[1]])
        var = onnx.helper.make_tensor_value_info("var", data_type, [input_shape[1]])
        y = onnx.helper.make_tensor_value_info("Y", data_type, input_shape)

        graph = onnx.helper.make_graph(
            nodes, "test_batchnormalization", [x, scale, B, mean, var], [y]
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "BatchNormalization"
        assert converted_model.opset_import[0].version == to_opset

    # Test Constant Adapter: 8 -> 9
    def test_constant_8_9(self) -> None:
        from_opset = 8
        to_opset = 9
        data_type = TensorProto.FLOAT

        output_shape = [2, 3, 4]
        output_value = np.arange(24)

        nodes = [
            helper.make_node(
                "Constant",
                inputs=[],
                outputs=["Y"],
                value=helper.make_tensor("", data_type, output_shape, output_value),
            )
        ]

        graph = helper.make_graph(
            nodes,
            "test_constant",
            [],
            [onnx.helper.make_tensor_value_info("Y", data_type, output_shape)],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "Constant"
        assert converted_model.graph.output[0].type.tensor_type.elem_type == data_type
        assert converted_model.opset_import[0].version == to_opset

    # Test Constant Adapter: 9 -> 8
    def test_constant_9_8(self) -> None:
        from_opset = 9
        to_opset = 8
        data_type = TensorProto.UINT64

        output_shape = [2, 3, 4]
        output_value = np.arange(24)

        nodes = [
            helper.make_node(
                "Constant",
                inputs=[],
                outputs=["Y"],
                value=helper.make_tensor("", data_type, output_shape, output_value),
            )
        ]

        graph = helper.make_graph(
            nodes,
            "test_constant",
            [],
            [onnx.helper.make_tensor_value_info("Y", data_type, output_shape)],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "Constant"
        assert converted_model.graph.output[0].type.tensor_type.elem_type == data_type
        assert converted_model.opset_import[0].version == to_opset

    # Test Flatten Adapter: 8 -> 9
    def test_flatten_8_9(self) -> None:
        from_opset = 8
        to_opset = 9
        data_type = TensorProto.FLOAT

        nodes = [onnx.helper.make_node("Flatten", inputs=["X"], outputs=["Y"], axis=1)]

        graph = helper.make_graph(
            nodes,
            "test_flatten",
            [onnx.helper.make_tensor_value_info("X", data_type, [2, 3, 4])],
            [onnx.helper.make_tensor_value_info("Y", data_type, [2, 12])],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "Flatten"
        assert converted_model.graph.output[0].type.tensor_type.elem_type == data_type
        assert converted_model.opset_import[0].version == to_opset

    # Test Flatten Adapter: 9 -> 8
    def test_flatten_9_8(self) -> None:
        from_opset = 9
        to_opset = 8
        data_type = TensorProto.UINT64

        nodes = [onnx.helper.make_node("Flatten", inputs=["X"], outputs=["Y"], axis=1)]

        graph = helper.make_graph(
            nodes,
            "test_flatten",
            [onnx.helper.make_tensor_value_info("X", data_type, [2, 3, 4])],
            [onnx.helper.make_tensor_value_info("Y", data_type, [2, 12])],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[1].op_type == "Flatten"
        assert converted_model.graph.output[0].type.tensor_type.elem_type == data_type
        assert converted_model.opset_import[0].version == to_opset

    # Test PRelu Adapter: 8 -> 9
    def test_prelu_8_9(self) -> None:
        from_opset = 8
        to_opset = 9
        data_type = TensorProto.FLOAT

        nodes = [onnx.helper.make_node("PRelu", inputs=["X", "Slope"], outputs=["Y"])]

        input_shape = [2, 3, 4]
        graph = helper.make_graph(
            nodes,
            "test_prelu",
            [
                onnx.helper.make_tensor_value_info("X", data_type, input_shape),
                onnx.helper.make_tensor_value_info("Slope", data_type, input_shape),
            ],
            [onnx.helper.make_tensor_value_info("Y", data_type, input_shape)],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "PRelu"
        assert converted_model.graph.output[0].type.tensor_type.elem_type == data_type
        assert converted_model.opset_import[0].version == to_opset

    # Test PRelu Adapter: 9 -> 8
    def test_prelu_9_8(self) -> None:
        from_opset = 9
        to_opset = 8
        data_type = TensorProto.UINT64

        nodes = [onnx.helper.make_node("PRelu", inputs=["X", "Slope"], outputs=["Y"])]

        input_shape = [2, 3, 4]
        graph = helper.make_graph(
            nodes,
            "test_prelu",
            [
                onnx.helper.make_tensor_value_info("X", data_type, input_shape),
                onnx.helper.make_tensor_value_info("Slope", data_type, input_shape),
            ],
            [onnx.helper.make_tensor_value_info("Y", data_type, input_shape)],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[2].op_type == "PRelu"
        assert converted_model.graph.output[0].type.tensor_type.elem_type == data_type
        assert converted_model.opset_import[0].version == to_opset

    # Test Greater Adapter: 8 -> 9
    def test_greater_8_9(self) -> None:
        from_opset = 8
        to_opset = 9
        data_type = TensorProto.FLOAT

        nodes = [onnx.helper.make_node("Greater", inputs=["X1", "X2"], outputs=["Y"])]

        input_shape = [2, 3, 4]
        graph = helper.make_graph(
            nodes,
            "test_greater",
            [
                onnx.helper.make_tensor_value_info("X1", data_type, input_shape),
                onnx.helper.make_tensor_value_info("X2", data_type, input_shape),
            ],
            [onnx.helper.make_tensor_value_info("Y", TensorProto.BOOL, input_shape)],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "Greater"
        assert (
            converted_model.graph.output[0].type.tensor_type.elem_type
            == TensorProto.BOOL
        )
        assert converted_model.opset_import[0].version == to_opset

    # Test Greater Adapter: 9 -> 8
    def test_greater_9_8(self) -> None:
        from_opset = 9
        to_opset = 8
        data_type = TensorProto.UINT64

        nodes = [onnx.helper.make_node("Greater", inputs=["X1", "X2"], outputs=["Y"])]

        input_shape = [2, 3, 4]
        graph = helper.make_graph(
            nodes,
            "test_greater",
            [
                onnx.helper.make_tensor_value_info("X1", data_type, input_shape),
                onnx.helper.make_tensor_value_info("X2", data_type, input_shape),
            ],
            [onnx.helper.make_tensor_value_info("Y", TensorProto.BOOL, input_shape)],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[2].op_type == "Greater"
        assert (
            converted_model.graph.output[0].type.tensor_type.elem_type
            == TensorProto.BOOL
        )
        assert converted_model.opset_import[0].version == to_opset

    # Test Less Adapter: 8 -> 9
    def test_less_8_9(self) -> None:
        from_opset = 8
        to_opset = 9
        data_type = TensorProto.FLOAT

        nodes = [onnx.helper.make_node("Less", inputs=["X1", "X2"], outputs=["Y"])]

        input_shape = [2, 3, 4]
        graph = helper.make_graph(
            nodes,
            "test_less",
            [
                onnx.helper.make_tensor_value_info("X1", data_type, input_shape),
                onnx.helper.make_tensor_value_info("X2", data_type, input_shape),
            ],
            [onnx.helper.make_tensor_value_info("Y", TensorProto.BOOL, input_shape)],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "Less"
        assert (
            converted_model.graph.output[0].type.tensor_type.elem_type
            == TensorProto.BOOL
        )
        assert converted_model.opset_import[0].version == to_opset

    # Test Less Adapter: 9 -> 8
    def test_less_9_8(self) -> None:
        from_opset = 9
        to_opset = 8
        data_type = TensorProto.UINT64

        nodes = [onnx.helper.make_node("Less", inputs=["X1", "X2"], outputs=["Y"])]

        input_shape = [2, 3, 4]
        graph = helper.make_graph(
            nodes,
            "test_less",
            [
                onnx.helper.make_tensor_value_info("X1", data_type, input_shape),
                onnx.helper.make_tensor_value_info("X2", data_type, input_shape),
            ],
            [onnx.helper.make_tensor_value_info("Y", TensorProto.BOOL, input_shape)],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[2].op_type == "Less"
        assert (
            converted_model.graph.output[0].type.tensor_type.elem_type
            == TensorProto.BOOL
        )
        assert converted_model.opset_import[0].version == to_opset

    # Test MatMul Adapter: 8 -> 9
    def test_matmul_8_9(self) -> None:
        from_opset = 8
        to_opset = 9
        data_type = TensorProto.FLOAT

        nodes = [onnx.helper.make_node("MatMul", inputs=["X1", "X2"], outputs=["Y"])]

        graph = helper.make_graph(
            nodes,
            "test_matmul",
            [
                onnx.helper.make_tensor_value_info("X1", data_type, [3, 4]),
                onnx.helper.make_tensor_value_info("X2", data_type, [4, 3]),
            ],
            [onnx.helper.make_tensor_value_info("Y", data_type, [3, 3])],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "MatMul"
        assert converted_model.graph.output[0].type.tensor_type.elem_type == data_type
        assert converted_model.opset_import[0].version == to_opset

    # Test MatMul Adapter: 9 -> 8
    def test_matmul_9_8(self) -> None:
        from_opset = 9
        to_opset = 8
        data_type = TensorProto.UINT64

        nodes = [onnx.helper.make_node("MatMul", inputs=["X1", "X2"], outputs=["Y"])]

        graph = helper.make_graph(
            nodes,
            "test_matmul",
            [
                onnx.helper.make_tensor_value_info("X1", data_type, [3, 4]),
                onnx.helper.make_tensor_value_info("X2", data_type, [4, 3]),
            ],
            [onnx.helper.make_tensor_value_info("Y", data_type, [3, 3])],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[2].op_type == "MatMul"
        assert converted_model.graph.output[0].type.tensor_type.elem_type == data_type
        assert converted_model.opset_import[0].version == to_opset

    # Test Gemm Adapter: 8 -> 9
    def test_gemm_8_9(self) -> None:
        from_opset = 8
        to_opset = 9
        data_type = TensorProto.FLOAT

        nodes = [
            onnx.helper.make_node("Gemm", inputs=["X1", "X2", "X3"], outputs=["Y"])
        ]

        graph = helper.make_graph(
            nodes,
            "test_gemm",
            [
                onnx.helper.make_tensor_value_info("X1", data_type, [3, 4]),
                onnx.helper.make_tensor_value_info("X2", data_type, [4, 3]),
                onnx.helper.make_tensor_value_info("X3", data_type, [3, 3]),
            ],
            [onnx.helper.make_tensor_value_info("Y", data_type, [3, 3])],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "Gemm"
        assert converted_model.graph.output[0].type.tensor_type.elem_type == data_type
        assert converted_model.opset_import[0].version == to_opset

    # Test Gemm Adapter: 9 -> 8
    def test_gemm_9_8(self) -> None:
        from_opset = 9
        to_opset = 8
        data_type = TensorProto.UINT64

        nodes = [
            onnx.helper.make_node("Gemm", inputs=["X1", "X2", "X3"], outputs=["Y"])
        ]

        graph = helper.make_graph(
            nodes,
            "test_gemm",
            [
                onnx.helper.make_tensor_value_info("X1", data_type, [3, 4]),
                onnx.helper.make_tensor_value_info("X2", data_type, [4, 3]),
                onnx.helper.make_tensor_value_info("X3", data_type, [3, 3]),
            ],
            [onnx.helper.make_tensor_value_info("Y", data_type, [3, 3])],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[3].op_type == "Gemm"
        assert converted_model.graph.output[0].type.tensor_type.elem_type == data_type
        assert converted_model.opset_import[0].version == to_opset

    # Test Upsample Adapter: 8 -> 9
    def test_upsample_8_9(self) -> None:
        from_opset = 8
        to_opset = 9
        data_type = TensorProto.FLOAT

        nodes = [
            onnx.helper.make_node(
                "Upsample",
                inputs=["X"],
                outputs=["Y"],
                mode="nearest",
                scales=[1.0, 1.0, 2.0, 3.0],
            )
        ]

        graph = helper.make_graph(
            nodes,
            "test_upsample_8_9",
            [onnx.helper.make_tensor_value_info("X", data_type, [1, 1, 2, 2])],
            [onnx.helper.make_tensor_value_info("Y", data_type, [1, 1, 4, 6])],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert len(converted_model.graph.node) == 2
        assert converted_model.graph.node[0].op_type == "Constant"
        assert converted_model.graph.node[1].op_type == "Upsample"
        assert len(converted_model.graph.node[1].attribute) == 1
        assert converted_model.graph.node[1].attribute[0].name == "mode"
        assert converted_model.opset_import[0].version == to_opset

    # Test Helper for Upsample Adapter: 9 -> 8
    def helper_upsample_with_initializer(self, raw_scale: bool = False) -> None:
        from_opset = 9
        to_opset = 8
        data_type = TensorProto.FLOAT

        nodes = [
            onnx.helper.make_node(
                "Upsample", inputs=["X", "Scales"], outputs=["Y"], mode="nearest"
            )
        ]

        scale_value = [1.0, 1.0, 2.0, 3.0]
        scale_tensor = onnx.helper.make_tensor(
            "Scales",
            onnx.TensorProto.FLOAT,
            [4],
            bytes(struct.pack("4f", *scale_value)) if raw_scale else scale_value,
            raw_scale,
        )

        graph = helper.make_graph(
            nodes,
            "test_upsample",
            [
                onnx.helper.make_tensor_value_info("X", data_type, [1, 1, 2, 2]),
                onnx.helper.make_tensor_value_info("Scales", data_type, [4]),
            ],
            [onnx.helper.make_tensor_value_info("Y", data_type, [1, 1, 4, 6])],
            [scale_tensor],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "Upsample"
        assert len(converted_model.graph.initializer) == 0
        assert len(converted_model.graph.node[0].attribute) == 2
        assert converted_model.graph.node[0].attribute[1].name == "scales"
        assert converted_model.opset_import[0].version == to_opset

    # Test Helper for Upsample Adapter: 9 -> 8
    def helper_upsample_with_constant(self, raw_scale: bool = False) -> None:
        from_opset = 9
        to_opset = 8
        data_type = TensorProto.FLOAT

        scale_value = [1.0, 1.0, 2.0, 3.0]
        scale_tensor = onnx.helper.make_tensor(
            "const_value",
            onnx.TensorProto.FLOAT,
            [4],
            bytes(struct.pack("4f", *scale_value)) if raw_scale else scale_value,
            raw_scale,
        )
        nodes = [
            onnx.helper.make_node(
                "Constant", inputs=[], outputs=["Constant_Output"], value=scale_tensor
            ),
            onnx.helper.make_node(
                "Upsample",
                inputs=["X", "Constant_Output"],
                outputs=["Y"],
                mode="nearest",
            ),
        ]

        graph = helper.make_graph(
            nodes,
            "test_upsample",
            [onnx.helper.make_tensor_value_info("X", data_type, [1, 1, 2, 2])],
            [onnx.helper.make_tensor_value_info("Y", data_type, [1, 1, 4, 6])],
            value_info=[
                onnx.helper.make_tensor_value_info("Constant_Output", data_type, [4])
            ],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert len(converted_model.graph.node) == 1
        assert converted_model.graph.node[0].op_type == "Upsample"
        assert len(converted_model.graph.node[0].attribute) == 2
        assert converted_model.graph.node[0].attribute[1].name == "scales"
        assert converted_model.opset_import[0].version == to_opset

    # Test Upsample Adapter: 9 -> 8
    def test_upsample_with_constant_node_9_8(self) -> None:
        self.helper_upsample_with_constant(raw_scale=False)

    # Test Upsample Adapter: 9 -> 8
    def test_upsample_with_initializer_9_8(self) -> None:
        self.helper_upsample_with_initializer(raw_scale=False)

    # Test Upsample Adapter: 9 -> 8
    def test_upsample_with_raw_initializer_9_8(self) -> None:
        self.helper_upsample_with_constant(raw_scale=True)

    # Test Upsample Adapter: 9 -> 8
    def test_upsample_with_raw_constant_node_9_8(self) -> None:
        self.helper_upsample_with_constant(raw_scale=True)

    # Test Scan Adapter: 8 -> 9
    def test_scan_8_9(self) -> None:
        from_opset = 8
        to_opset = 9
        data_type = TensorProto.FLOAT

        node1 = onnx.helper.make_node(
            "Add",
            inputs=["sum_in", "next"],
            outputs=["sum_out"],
        )
        node2 = onnx.helper.make_node(
            "Identity",
            inputs=["sum_out"],
            outputs=["scan_out"],
        )
        g = onnx.helper.make_graph(
            [node1, node2],
            "scan_body",
            [
                onnx.helper.make_tensor_value_info("sum_in", data_type, [2]),
                onnx.helper.make_tensor_value_info("next", data_type, [2]),
            ],
            [
                onnx.helper.make_tensor_value_info("sum_out", data_type, [2]),
                onnx.helper.make_tensor_value_info("scan_out", data_type, [2]),
            ],
        )

        no_sequence_lens = ""  # optional input, not supplied
        nodes = [
            onnx.helper.make_node(
                "Scan",
                inputs=[no_sequence_lens, "initial", "x"],
                outputs=["y", "z"],
                body=g,
                num_scan_inputs=1,
            )
        ]

        initial = onnx.helper.make_tensor_value_info("initial", data_type, [1, 2])
        x = onnx.helper.make_tensor_value_info("x", data_type, [1, 3, 2])
        y = onnx.helper.make_tensor_value_info("y", data_type, [1, 2])
        z = onnx.helper.make_tensor_value_info("z", data_type, [1, 3, 2])

        graph = onnx.helper.make_graph(nodes, "test_scan_8_9", [initial, x], [y, z])

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "Scan"
        assert converted_model.opset_import[0].version == to_opset

    # Test Cast Adapter: 8 -> 9
    def test_cast_8_9(self) -> None:
        from_opset = 8
        to_opset = 9
        data_type_from = TensorProto.FLOAT
        data_type_to = TensorProto.UINT32

        nodes = [
            onnx.helper.make_node(
                "Cast", inputs=["X"], outputs=["Y"], to=TensorProto.UINT32
            )
        ]

        graph = helper.make_graph(
            nodes,
            "test_cast",
            [onnx.helper.make_tensor_value_info("X", data_type_from, [2, 3])],
            [onnx.helper.make_tensor_value_info("Y", data_type_to, [2, 3])],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "Cast"
        assert (
            converted_model.graph.output[0].type.tensor_type.elem_type == data_type_to
        )
        assert converted_model.opset_import[0].version == to_opset

    # Test Split Adapter: 13 -> 12
    def test_split_13_12(self) -> None:
        nodes = [
            helper.make_node(
                "Constant",
                [],
                ["split"],
                value=helper.make_tensor("", TensorProto.INT64, [2], [2, 3]),
            ),
            helper.make_node("Split", ["X", "split"], ["Y1", "Y2"]),
        ]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (5,))],
            [
                helper.make_tensor_value_info("Y1", TensorProto.FLOAT, (2,)),
                helper.make_tensor_value_info("Y2", TensorProto.FLOAT, (3,)),
            ],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 13), 12)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Split"
        assert converted_model.opset_import[0].version == 12

    def test_split_with_optional_input(self) -> None:
        nodes = [helper.make_node("Split", ["X"], ["Y1", "Y2"], axis=1)]
        graph = helper.make_graph(
            nodes,
            "test_split_optional_input",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (6,))],
            [
                helper.make_tensor_value_info("Y1", TensorProto.FLOAT, (3,)),
                helper.make_tensor_value_info("Y2", TensorProto.FLOAT, (3,)),
            ],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 12), 18)

        assert converted_model.graph.node[0].op_type == "Split"
        assert converted_model.opset_import[0].version == 18

        assert len(converted_model.graph.node[0].output) == 2

    # Test Split Adapter: 12 -> 13
    def test_split_12_13(self) -> None:
        nodes = [helper.make_node("Split", ["X"], ["Y1", "Y2"], split=[2, 3])]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (5,))],
            [
                helper.make_tensor_value_info("Y1", TensorProto.FLOAT, (2,)),
                helper.make_tensor_value_info("Y2", TensorProto.FLOAT, (3,)),
            ],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 12), 13)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Constant"
        assert converted_model.graph.node[1].op_type == "Split"
        assert converted_model.opset_import[0].version == 13

    # Test Split Adapter: 13 -> 12 with optional split input
    def test_split_13_12_optional_input(self) -> None:
        """Test Split 13->12 conversion with optional split input."""
        nodes = [helper.make_node("Split", ["X"], ["Y1", "Y2"], axis=0)]
        graph = helper.make_graph(
            nodes,
            "test_split_13_12_optional",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (10,))],
            [
                helper.make_tensor_value_info("Y1", TensorProto.FLOAT, (5,)),
                helper.make_tensor_value_info("Y2", TensorProto.FLOAT, (5,)),
            ],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 13), 12)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Split"
        assert converted_model.opset_import[0].version == 12

    # Test AxesInputToAttribute Adapter: 13 -> 12
    def test_axes_input_to_attr_13_12(self) -> None:
        nodes = [
            helper.make_node(
                "Constant",
                [],
                ["axes"],
                value=helper.make_tensor("", TensorProto.INT64, [1], [0]),
            ),
            helper.make_node("ReduceSum", ["X", "axes"], ["Y"]),
        ]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (5, 5))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (1, 5))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 13), 12)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "ReduceSum"
        assert converted_model.opset_import[0].version == 12

    # Test AxesAttributeToInput Adapter: 12 -> 13
    def test_axes_attr_to_input_12_13(self) -> None:
        nodes = [helper.make_node("ReduceSum", ["X"], ["Y"], axes=[0])]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (5, 5))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (1, 5))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 12), 13)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Constant"
        assert converted_model.opset_import[0].version == 13

    # Test AxesInputToAttribute Adapter: 13 -> 11 with optional axes input
    def test_squeeze_13_11_optional_axes(self) -> None:
        """Test Squeeze 13->11 conversion with optional axes input."""
        nodes = [helper.make_node("Squeeze", ["X"], ["Y"])]
        graph = helper.make_graph(
            nodes,
            "test_squeeze_13_11_optional",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (1, 10, 1))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (10,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 13), 11)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Squeeze"
        assert converted_model.opset_import[0].version == 11

    # Test Reshape Adapter: 4 -> 5 with optional axes input
    def test_reshape_4_5_optional_shape(self) -> None:
        """Test Reshape 4->5 conversion - shape in attribute should become input."""
        nodes = [helper.make_node("Reshape", ["X"], ["Y"], shape=[2, 5])]
        graph = helper.make_graph(
            nodes,
            "test_reshape_4_5",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (10,))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (2, 5))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 4), 5)
        # Should have added a Constant node for the shape
        assert converted_model.graph.node[0].op_type == "Constant"
        assert converted_model.graph.node[1].op_type == "Reshape"
        assert converted_model.opset_import[0].version == 5

    # Test Resize Adapter: 10 -> 11
    def test_resize_10_11_bounds_check(self) -> None:
        """Test Resize 10->11 conversion with proper bounds checking."""
        nodes = [
            helper.make_node(
                "Constant",
                [],
                ["scales"],
                value=helper.make_tensor(
                    "", TensorProto.FLOAT, [4], [1.0, 1.0, 2.0, 2.0]
                ),
            ),
            helper.make_node("Resize", ["X", "scales"], ["Y"], mode="nearest"),
        ]
        graph = helper.make_graph(
            nodes,
            "test_resize_10_11",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (1, 1, 2, 2))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (1, 1, 4, 4))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 10), 11)
        assert converted_model.opset_import[0].version == 11

    # Test Scatter Adapter: 10 -> 11
    def test_scatter_10_11_bounds_check(self) -> None:
        """Test Scatter 10->11 conversion with proper bounds checking."""
        nodes = [
            helper.make_node("Scatter", ["data", "indices", "updates"], ["Y"], axis=0)
        ]
        graph = helper.make_graph(
            nodes,
            "test_scatter_10_11",
            [
                helper.make_tensor_value_info("data", TensorProto.FLOAT, (3,)),
                helper.make_tensor_value_info("indices", TensorProto.INT64, (2,)),
                helper.make_tensor_value_info("updates", TensorProto.FLOAT, (2,)),
            ],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (3,))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 10), 11)
        assert converted_model.graph.node[0].op_type == "ScatterElements"
        assert converted_model.opset_import[0].version == 11

    # Test Slice Adapter: 9 -> 10
    def test_slice_9_10(self) -> None:
        nodes = [
            helper.make_node(
                "Slice", ["X"], ["Y"], axes=[0, 1], starts=[0, 0], ends=[3, 10]
            )
        ]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (20, 10, 5))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (3, 10, 5))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 9), 10)

        assert converted_model.graph.node[0].op_type == "Constant"
        assert converted_model.graph.node[1].op_type == "Constant"
        assert converted_model.graph.node[2].op_type == "Constant"
        assert converted_model.graph.node[3].op_type == "Slice"
        assert converted_model.opset_import[0].version == 10
        assert len(converted_model.graph.node[3].input) == 4
        assert len(converted_model.graph.node[3].attribute) == 0

    # Test RNN Adapter: 13 -> 14
    def test_rnn_13_14(self) -> None:
        from_opset = 13
        to_opset = 14
        data_type = TensorProto.FLOAT

        seq_length = 1
        batch_size = 2
        input_size = 3
        num_directions = 1
        hidden_size = 5

        nodes = [
            onnx.helper.make_node(
                "RNN",
                inputs=["X", "W", "R"],
                outputs=["", "Y_h"],
                hidden_size=hidden_size,
            )
        ]

        graph = helper.make_graph(
            nodes,
            "test_rnn",
            [
                onnx.helper.make_tensor_value_info(
                    "X", data_type, [seq_length, batch_size, input_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "W", data_type, [num_directions, hidden_size, input_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "R", data_type, [num_directions, hidden_size, hidden_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "B", data_type, [num_directions, 2 * hidden_size]
                ),
            ],
            [
                onnx.helper.make_tensor_value_info(
                    "Y_h", data_type, [num_directions, batch_size, hidden_size]
                )
            ],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "RNN"
        assert converted_model.opset_import[0].version == to_opset
        assert len(converted_model.graph.node[0].attribute) == 2
        assert converted_model.graph.node[0].attribute[1].name == "layout"

    # Test GRU Adapter: 13 -> 14
    def test_gru_13_14(self) -> None:
        from_opset = 13
        to_opset = 14
        data_type = TensorProto.FLOAT

        seq_length = 1
        batch_size = 2
        input_size = 3
        num_directions = 1
        hidden_size = 5

        nodes = [
            onnx.helper.make_node(
                "GRU",
                inputs=["X", "W", "R"],
                outputs=["", "Y_h"],
                hidden_size=hidden_size,
            )
        ]

        graph = helper.make_graph(
            nodes,
            "test_gru",
            [
                onnx.helper.make_tensor_value_info(
                    "X", data_type, [seq_length, batch_size, input_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "W", data_type, [num_directions, 3 * hidden_size, input_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "R", data_type, [num_directions, 3 * hidden_size, hidden_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "B", data_type, [num_directions, 6 * hidden_size]
                ),
            ],
            [
                onnx.helper.make_tensor_value_info(
                    "Y_h", data_type, [num_directions, batch_size, hidden_size]
                )
            ],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "GRU"
        assert converted_model.opset_import[0].version == to_opset
        assert len(converted_model.graph.node[0].attribute) == 2
        assert converted_model.graph.node[0].attribute[1].name == "layout"

    # Test LSTM Adapter: 13 -> 14
    def test_lstm_13_14(self) -> None:
        from_opset = 13
        to_opset = 14
        data_type = TensorProto.FLOAT

        seq_length = 1
        batch_size = 2
        input_size = 3
        num_directions = 1
        hidden_size = 5

        nodes = [
            onnx.helper.make_node(
                "LSTM",
                inputs=["X", "W", "R"],
                outputs=["", "Y_h"],
                hidden_size=hidden_size,
            )
        ]

        graph = helper.make_graph(
            nodes,
            "test_lstm",
            [
                onnx.helper.make_tensor_value_info(
                    "X", data_type, [seq_length, batch_size, input_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "W", data_type, [num_directions, 4 * hidden_size, input_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "R", data_type, [num_directions, 4 * hidden_size, hidden_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "B", data_type, [num_directions, 8 * hidden_size]
                ),
            ],
            [
                onnx.helper.make_tensor_value_info(
                    "Y_h", data_type, [num_directions, batch_size, hidden_size]
                )
            ],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "LSTM"
        assert converted_model.opset_import[0].version == to_opset
        assert len(converted_model.graph.node[0].attribute) == 2
        assert converted_model.graph.node[0].attribute[1].name == "layout"

    # Test RNN Adapter: 14 -> 13
    def test_rnn_14_13(self) -> None:
        from_opset = 14
        to_opset = 13
        data_type = TensorProto.FLOAT

        seq_length = 1
        batch_size = 2
        input_size = 3
        num_directions = 1
        hidden_size = 5

        nodes = [
            onnx.helper.make_node(
                "RNN",
                inputs=["X", "W", "R"],
                outputs=["", "Y_h"],
                hidden_size=hidden_size,
                layout=0,
            )
        ]

        graph = helper.make_graph(
            nodes,
            "test_rnn",
            [
                onnx.helper.make_tensor_value_info(
                    "X", data_type, [seq_length, batch_size, input_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "W", data_type, [num_directions, hidden_size, input_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "R", data_type, [num_directions, hidden_size, hidden_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "B", data_type, [num_directions, 2 * hidden_size]
                ),
            ],
            [
                onnx.helper.make_tensor_value_info(
                    "Y_h", data_type, [num_directions, batch_size, hidden_size]
                )
            ],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "RNN"
        assert converted_model.opset_import[0].version == to_opset
        assert len(converted_model.graph.node[0].attribute) == 1

    # Test GRU Adapter: 14 -> 13
    def test_gru_14_13(self) -> None:
        from_opset = 14
        to_opset = 13
        data_type = TensorProto.FLOAT

        seq_length = 1
        batch_size = 2
        input_size = 3
        num_directions = 1
        hidden_size = 5

        nodes = [
            onnx.helper.make_node(
                "GRU",
                inputs=["X", "W", "R"],
                outputs=["", "Y_h"],
                hidden_size=hidden_size,
                layout=0,
            )
        ]

        graph = helper.make_graph(
            nodes,
            "test_gru",
            [
                onnx.helper.make_tensor_value_info(
                    "X", data_type, [seq_length, batch_size, input_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "W", data_type, [num_directions, 3 * hidden_size, input_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "R", data_type, [num_directions, 3 * hidden_size, hidden_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "B", data_type, [num_directions, 6 * hidden_size]
                ),
            ],
            [
                onnx.helper.make_tensor_value_info(
                    "Y_h", data_type, [num_directions, batch_size, hidden_size]
                )
            ],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "GRU"
        assert converted_model.opset_import[0].version == to_opset
        assert len(converted_model.graph.node[0].attribute) == 1

    # Test LSTM Adapter: 14 -> 13
    def test_lstm_14_13(self) -> None:
        from_opset = 14
        to_opset = 13
        data_type = TensorProto.FLOAT

        seq_length = 1
        batch_size = 2
        input_size = 3
        num_directions = 1
        hidden_size = 5

        nodes = [
            onnx.helper.make_node(
                "LSTM",
                inputs=["X", "W", "R"],
                outputs=["", "Y_h"],
                hidden_size=hidden_size,
                layout=0,
            )
        ]

        graph = helper.make_graph(
            nodes,
            "test_lstm",
            [
                onnx.helper.make_tensor_value_info(
                    "X", data_type, [seq_length, batch_size, input_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "W", data_type, [num_directions, 4 * hidden_size, input_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "R", data_type, [num_directions, 4 * hidden_size, hidden_size]
                ),
                onnx.helper.make_tensor_value_info(
                    "B", data_type, [num_directions, 8 * hidden_size]
                ),
            ],
            [
                onnx.helper.make_tensor_value_info(
                    "Y_h", data_type, [num_directions, batch_size, hidden_size]
                )
            ],
        )

        converted_model = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted_model.graph.node[0].op_type == "LSTM"
        assert converted_model.opset_import[0].version == to_opset
        assert len(converted_model.graph.node[0].attribute) == 1

    # Test Pad Adapter: 10 -> 11
    def test_pad_10_11(self) -> None:
        pads = (0, 1, 2, 0, 2, 1)
        nodes = [helper.make_node("Pad", ["X"], ["Y"], pads=pads)]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (1, 2, 2))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (1, 5, 5))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 10), 11)

        # Assert equality of graph and converted_model
        assert converted_model.graph.node[1].op_type == "Pad"
        assert converted_model.opset_import[0].version == 11

    def test_pad_with_value_10_11(self) -> None:
        pads = (0, 1, 2, 0, 2, 1)
        nodes = [helper.make_node("Pad", ["X"], ["Y"], pads=pads, value=1.0)]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (1, 2, 2))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (1, 5, 5))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 10), 11)

        # Assert equality of graph and converted_model
        assert converted_model.graph.node[1].op_type == "Pad"
        assert converted_model.opset_import[0].version == 11

    # Test that subgraphs are converted
    def test_if_subgraph_10_11(self) -> None:
        from_opset = 10
        to_opset = 11
        data_type = TensorProto.FLOAT
        data_shape = [2]

        subg1_node = [
            onnx.helper.make_node(
                "Clip", inputs=["sub_in"], outputs=["sub_out"], min=2.0, max=3.0
            )
        ]
        subg1_input = [
            onnx.helper.make_tensor_value_info("sub_in", data_type, data_shape)
        ]
        subg1_output = [
            onnx.helper.make_tensor_value_info("sub_out", data_type, data_shape)
        ]
        subg1 = helper.make_graph(subg1_node, "then_g", subg1_input, subg1_output)

        subg2_node = [
            onnx.helper.make_node(
                "Clip", inputs=["sub_in"], outputs=["sub_out"], min=2.0, max=3.0
            )
        ]
        subg2_input = [
            onnx.helper.make_tensor_value_info("sub_in", data_type, data_shape)
        ]
        subg2_output = [
            onnx.helper.make_tensor_value_info("sub_out", data_type, data_shape)
        ]
        subg2 = helper.make_graph(subg2_node, "then_g", subg2_input, subg2_output)

        node = [
            onnx.helper.make_node(
                "If",
                inputs=["cond"],
                outputs=["out"],
                then_branch=subg1,
                else_branch=subg2,
            )
        ]
        input = [onnx.helper.make_tensor_value_info("cond", TensorProto.BOOL, [])]
        output = [onnx.helper.make_tensor_value_info("out", data_type, data_shape)]
        init = [helper.make_tensor("sub_in", data_type, data_shape, [4.0, 5.0])]
        graph = helper.make_graph(node, "test_subgraphs", input, output, init)

        converted = self._converted(
            graph, helper.make_operatorsetid("", from_opset), to_opset
        )

        assert converted.graph.node[0].op_type == "If"
        assert converted.opset_import[0].version == to_opset
        assert converted.graph.node[0].attribute[0].g.node[2].op_type == "Clip"
        assert len(converted.graph.node[0].attribute[0].g.node[2].attribute) == 0
        assert converted.graph.node[0].attribute[1].g.node[2].op_type == "Clip"
        assert len(converted.graph.node[0].attribute[1].g.node[2].attribute) == 0

    # Use initializer as node inputs (instead of graph input)
    # to test whether IR (version_converter) can handle it
    def test_initializer_not_in_input_above_ir4(self):  # type: () -> None
        nodes = [
            helper.make_node(
                "BatchNormalization", ["X", "scale", "B", "mean", "var"], ["Y"]
            )
        ]

        scale_value = [0.55, 0.72]
        scale_tensor = onnx.helper.make_tensor(
            "scale", onnx.TensorProto.FLOAT, [2], scale_value
        )
        b_value = [0.60, 0.54]
        b_tensor = onnx.helper.make_tensor("B", onnx.TensorProto.FLOAT, [2], b_value)
        mean_value = [0.42, 0.65]
        mean_tensor = onnx.helper.make_tensor(
            "mean", onnx.TensorProto.FLOAT, [2], mean_value
        )
        var_value = [0.44, 0.89]
        var_tensor = onnx.helper.make_tensor(
            "var", onnx.TensorProto.FLOAT, [2], var_value
        )

        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (1, 2, 2, 3))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (1, 2, 2, 3))],
            [scale_tensor, b_tensor, mean_tensor, var_tensor],
        )

        converted_model = self._converted(graph, helper.make_operatorsetid("", 11), 12)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "BatchNormalization"
        assert converted_model.opset_import[0].version == 12

    def test_softmax_12_13(self) -> None:
        axis = 0
        nodes = [helper.make_node("Softmax", ["X"], ["Y"], axis=axis)]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (1, 2, 3))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (1, 2, 3))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 11), 13)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Shape"
        assert converted_model.graph.node[1].op_type == "Flatten"
        assert converted_model.graph.node[1].attribute[0].name == "axis"
        assert converted_model.graph.node[1].attribute[0].i == axis
        assert converted_model.graph.node[2].op_type == "Softmax"
        assert converted_model.graph.node[2].attribute[0].name == "axis"
        assert converted_model.graph.node[2].attribute[0].i == -1
        assert converted_model.graph.node[3].op_type == "Reshape"
        assert converted_model.opset_import[0].version == 13

    def test_softmax_13_12(self) -> None:
        axis = -1
        nodes = [helper.make_node("Softmax", ["X"], ["Y"], axis=axis)]
        graph = helper.make_graph(
            nodes,
            "test",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (1, 2, 3))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (1, 2, 3))],
        )
        converted_model = self._converted(graph, helper.make_operatorsetid("", 13), 12)
        # Assert equality of graph and converted_model
        assert converted_model.graph.node[0].op_type == "Softmax"
        assert converted_model.graph.node[0].attribute[0].name == "axis"
        assert converted_model.graph.node[0].attribute[0].i == 2
        assert converted_model.opset_import[0].version == 12

    @parameterized.parameterized.expand(
        [
            (
                "cast_9_8",
                "Cast",
                {"to": TensorProto.FLOAT},
                9,
                8,
                TensorProto.FLOAT,
                (1,),
            ),
            (
                "softmax_12_13",
                "Softmax",
                {"axis": 1},
                12,
                13,
                TensorProto.FLOAT,
                (1,),
            ),
            (
                "upsample_9_10",
                "Upsample",
                {"mode": "nearest"},
                9,
                10,
                TensorProto.FLOAT,
                (1, 1, 2, 2),
            ),
        ]
    )
    def test_rejects_missing_required_inputs(
        self,
        _: str,
        op_type: str,
        attrs: dict[str, int | str],
        from_opset: int,
        to_opset: int,
        output_type: int,
        output_shape: tuple[int, ...],
    ) -> None:
        def test() -> None:
            nodes = [helper.make_node(op_type, [], ["Y"], **attrs)]
            graph = helper.make_graph(
                nodes,
                f"test_{op_type.lower()}_{from_opset}_{to_opset}_rejects_missing_input",
                [],
                [helper.make_tensor_value_info("Y", output_type, output_shape)],
            )
            self._converted(graph, helper.make_operatorsetid("", from_opset), to_opset)

        self.assertRaises((RuntimeError, shape_inference.InferenceError), test)

    def test_softmax_13_12_rejects_malformed_flatten_input(self) -> None:
        def test() -> None:
            nodes = [
                helper.make_node("Flatten", [], ["F"], axis=1),
                helper.make_node("Softmax", ["F"], ["Y"], axis=0),
            ]
            graph = helper.make_graph(
                nodes,
                "test_softmax_13_12_rejects_malformed_flatten_input",
                [],
                [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (1,))],
            )
            self._converted(graph, helper.make_operatorsetid("", 13), 12)

        self.assertRaises((RuntimeError, shape_inference.InferenceError), test)

    @parameterized.parameterized.expand(
        [
            ("per_tensor", (16, 3), (1,), None, None, None, TensorProto.INT8, True),
            (
                "per_axis_none_block_shape",
                (16, 3),
                (16,),
                1,
                None,
                None,
                TensorProto.INT8,
                True,
            ),
            (
                "per_axis_zero_block_shape",
                (16, 3),
                (16,),
                1,
                0,
                None,
                TensorProto.INT8,
                True,
            ),
            (
                "per_tensor_positive_block_shape",
                (16, 3),
                (1,),
                1,
                2,
                None,
                TensorProto.INT8,
                False,
            ),
            (
                "per_axis_positive_block_shape",
                (16, 3),
                (16,),
                1,
                2,
                None,
                TensorProto.INT8,
                False,
            ),
            ("blocked_2d", (16, 3), (4, 3), 0, 4, None, TensorProto.INT8, False),
            ("blocked_3d", (4, 3, 32), (4, 3, 8), 2, 4, None, TensorProto.INT8, False),
            (
                "per_axis_output_dtype",
                (16, 3),
                (16,),
                1,
                None,
                TensorProto.FLOAT8E4M3FN,
                None,
                False,
            ),
            (
                "per_axis_unsupported_type",
                (16, 3),
                (16,),
                1,
                None,
                None,
                TensorProto.UINT16,
                False,
            ),
        ]
    )
    def test_quantize_21_20(
        self,
        _: str,
        x_shape: tuple[int, ...],
        scale_shape: tuple[int, ...],
        axis: int,
        block_size: int,
        output_dtype: int | None,
        zero_point_dtype: int | None,
        compatible: bool,
    ) -> None:
        def test(
            input_shape, scale_shape, axis, block_size, output_dtype, zero_point_dtype
        ) -> None:
            nodes = [
                helper.make_node(
                    "QuantizeLinear",
                    ["X", "S"],
                    ["Y"],
                    axis=axis,
                    block_size=block_size,
                    output_dtype=output_dtype,
                )
            ]
            inputs = [
                helper.make_tensor_value_info("X", TensorProto.FLOAT, input_shape),
                helper.make_tensor_value_info("S", TensorProto.FLOAT, scale_shape),
            ]
            if zero_point_dtype:
                inputs.append(
                    helper.make_tensor_value_info("ZP", zero_point_dtype, scale_shape)
                )
                nodes[0].input.append("ZP")
            output_type_ = output_dtype or zero_point_dtype
            graph = helper.make_graph(
                nodes,
                "test",
                inputs,
                [helper.make_tensor_value_info("Y", output_type_, input_shape)],
            )
            _ = self._converted(graph, helper.make_operatorsetid("", 21), 20)

        context_manager = (
            contextlib.nullcontext() if compatible else self.assertRaises(RuntimeError)
        )
        with context_manager:
            test(x_shape, scale_shape, axis, block_size, output_dtype, zero_point_dtype)

    @parameterized.parameterized.expand(
        [
            ("per_tensor", (16, 3), (1,), None, None, True),
            ("per_axis_none_block_shape", (16, 3), (16,), 1, None, True),
            ("per_axis_zero_block_shape", (16, 3), (16,), 1, 0, True),
            ("per_tensor_positive_block_shape", (16, 3), (1,), 1, 2, False),
            ("per_axis_positive_block_shape", (16, 3), (16,), 1, 2, False),
            ("blocked_2d", (16, 3), (4, 3), 0, 4, False),
            ("blocked_3d", (4, 3, 32), (4, 3, 8), 2, 4, False),
        ]
    )
    def test_dequantize_21_20(
        self,
        _: str,
        y_shape: tuple[int, ...],
        scale_shape: tuple[int, ...],
        axis: int,
        block_size: int,
        compatible: bool,
    ) -> None:
        def test(input_shape, scale_shape, axis, block_size) -> None:
            nodes = [
                helper.make_node(
                    "DequantizeLinear",
                    ["X", "S", "ZP"],
                    ["Y"],
                    axis=axis,
                    block_size=block_size,
                )
            ]
            graph = helper.make_graph(
                nodes,
                "test",
                [
                    helper.make_tensor_value_info("X", TensorProto.INT8, input_shape),
                    helper.make_tensor_value_info("S", TensorProto.FLOAT, scale_shape),
                    helper.make_tensor_value_info("ZP", TensorProto.INT8, scale_shape),
                ],
                [helper.make_tensor_value_info("Y", TensorProto.FLOAT, input_shape)],
            )
            _ = self._converted(graph, helper.make_operatorsetid("", 21), 20)

        context_manager = (
            contextlib.nullcontext() if compatible else self.assertRaises(RuntimeError)
        )
        with context_manager:
            test(y_shape, scale_shape, axis, block_size)

    def test_external_data_version_conversion(self) -> None:
        with tempfile.TemporaryDirectory() as temp_dir:
            # Create a model with external data
            shape = (2, 3)
            random_data = np.random.rand(*shape).astype(np.float32)

            initializer_tensor = onnx.helper.make_tensor(
                name="initializer_tensor",
                data_type=onnx.TensorProto.FLOAT,
                dims=list(shape),
                vals=random_data,
                raw=True,
            )
            initializer_scalar = onnx.helper.make_tensor(
                name="initializer_scalar",
                data_type=onnx.TensorProto.FLOAT,
                dims=[],
                vals=[1.0],
            )

            add_node = onnx.helper.make_node(
                "Add",
                inputs=["initializer_tensor", "initializer_scalar"],
                outputs=["sum_output"],
            )

            graph_def = onnx.helper.make_graph(
                name="SimpleAddition",
                nodes=[add_node],
                inputs=[],
                outputs=[
                    onnx.helper.make_tensor_value_info(
                        "sum_output", onnx.TensorProto.FLOAT, list(shape)
                    )
                ],
                initializer=[initializer_tensor, initializer_scalar],
            )

            # Save model to file with external data
            model_filename = os.path.join(temp_dir, "test_simple_add.onnx")
            data_filename = "test_simple_add.onnx.data"  # Use relative path
            opset_imports = [onnx.helper.make_opsetid("", 20)]
            model_def = onnx.helper.make_model(graph_def, opset_imports=opset_imports)
            model_def.ir_version = 10
            onnx.save_model(
                model_def,
                model_filename,
                save_as_external_data=True,
                all_tensors_to_one_file=True,
                location=data_filename,
                size_threshold=0,
                convert_attribute=False,
            )

            # Load the model and verify external data
            converted_model = onnx.version_converter.convert_version(
                onnx.load(model_filename, load_external_data=False), 21
            )
            self.assertEqual(len(converted_model.graph.initializer), 2)

            # Verify the large tensor has external data
            tensors = {init.name: init for init in converted_model.graph.initializer}
            self.assertIn("initializer_tensor", tensors)
            large_tensor = tensors["initializer_tensor"]
            self.assertEqual(large_tensor.data_location, TensorProto.EXTERNAL)
            self.assertEqual(len(large_tensor.external_data), 3)

            # Convert external_data to dictionary for order-independent checking
            external_data_dict = {ed.key: ed.value for ed in large_tensor.external_data}
            self.assertEqual(external_data_dict["location"], data_filename)
            self.assertEqual(external_data_dict["offset"], "0")
            self.assertEqual(external_data_dict["length"], "24")

    # Where 16 -> 15: TypeRestriction rejects bfloat16
    def test_where_16_15_success(self) -> None:
        nodes = [helper.make_node("Where", ["cond", "x", "y"], ["out"])]
        graph = helper.make_graph(
            nodes,
            "where",
            [
                helper.make_tensor_value_info("cond", TensorProto.BOOL, (2, 3)),
                helper.make_tensor_value_info("x", TensorProto.FLOAT, (2, 3)),
                helper.make_tensor_value_info("y", TensorProto.FLOAT, (2, 3)),
            ],
            [helper.make_tensor_value_info("out", TensorProto.FLOAT, (2, 3))],
        )
        converted = self._converted(graph, helper.make_operatorsetid("", 16), 15)
        assert converted.opset_import[0].version == 15

    def test_where_bfloat16_16_15_fails(self) -> None:
        def test() -> None:
            nodes = [helper.make_node("Where", ["cond", "x", "y"], ["out"])]
            graph = helper.make_graph(
                nodes,
                "where_bf16",
                [
                    helper.make_tensor_value_info("cond", TensorProto.BOOL, (2, 3)),
                    helper.make_tensor_value_info("x", TensorProto.BFLOAT16, (2, 3)),
                    helper.make_tensor_value_info("y", TensorProto.BFLOAT16, (2, 3)),
                ],
                [helper.make_tensor_value_info("out", TensorProto.BFLOAT16, (2, 3))],
            )
            self._converted(graph, helper.make_operatorsetid("", 16), 15)

        self.assertRaises(RuntimeError, test)

    def _make_scatter_graph(
        self,
        op_name: str,
        dtype: int = TensorProto.FLOAT,
        reduction: str | None = None,
    ) -> GraphProto:
        """Build a graph for ScatterElements or ScatterND with standard test shapes."""
        scatter_graph_config = {
            "ScatterElements": ((2, 3), (2, 3), (2, 3), {"axis": 0}),
            "ScatterND": ((4, 5), (2, 1), (2, 5), {}),
        }
        data_s, indices_s, updates_s, attrs = scatter_graph_config[op_name]
        attrs = dict(attrs)
        if reduction is not None:
            attrs["reduction"] = reduction
        nodes = [
            helper.make_node(op_name, ["data", "indices", "updates"], ["out"], **attrs)
        ]
        return helper.make_graph(
            nodes,
            op_name.lower(),
            [
                helper.make_tensor_value_info("data", dtype, data_s),
                helper.make_tensor_value_info("indices", TensorProto.INT64, indices_s),
                helper.make_tensor_value_info("updates", dtype, updates_s),
            ],
            [helper.make_tensor_value_info("out", dtype, data_s)],
        )

    # Scatter 16 -> 15: TypeRestriction (bfloat16) + RemoveAttribute (reduction)
    @parameterized.parameterized.expand([("ScatterElements",), ("ScatterND",)])
    def test_scatter_16_15_success(self, op_name: str) -> None:
        graph = self._make_scatter_graph(op_name, TensorProto.FLOAT, "none")
        converted = self._converted(graph, helper.make_operatorsetid("", 16), 15)
        assert converted.opset_import[0].version == 15

    @parameterized.parameterized.expand([("ScatterElements",), ("ScatterND",)])
    def test_scatter_16_15_bfloat16_fails(self, op_name: str) -> None:
        def test() -> None:
            graph = self._make_scatter_graph(op_name, TensorProto.BFLOAT16)
            self._converted(graph, helper.make_operatorsetid("", 16), 15)

        self.assertRaises(RuntimeError, test)

    # Opset 16 added reduction 'add' and 'mul'; 16 -> 15 only allows 'none'
    @parameterized.parameterized.expand(
        [
            ("ScatterElements", "add"),
            ("ScatterElements", "mul"),
            ("ScatterND", "add"),
            ("ScatterND", "mul"),
        ]
    )
    def test_scatter_16_15_reduction_add_mul_fails(
        self, op_name: str, reduction: str
    ) -> None:
        def test() -> None:
            graph = self._make_scatter_graph(op_name, TensorProto.FLOAT, reduction)
            self._converted(graph, helper.make_operatorsetid("", 16), 15)

        self.assertRaises(RuntimeError, test)

    # Scatter 18 -> 17: reject reduction "max" / "min"
    @parameterized.parameterized.expand(
        [
            ("ScatterElements", "max"),
            ("ScatterElements", "min"),
            ("ScatterND", "max"),
            ("ScatterND", "min"),
        ]
    )
    def test_scatter_18_17_reduction_max_min_fails(
        self, op_name: str, reduction: str
    ) -> None:
        def test() -> None:
            graph = self._make_scatter_graph(op_name, TensorProto.FLOAT, reduction)
            self._converted(graph, helper.make_operatorsetid("", 18), 17)

        self.assertRaises(RuntimeError, test)

    @parameterized.parameterized.expand(
        [
            ("ScatterElements", None),
            ("ScatterElements", "none"),
            ("ScatterElements", "add"),
            ("ScatterElements", "mul"),
            ("ScatterND", None),
            ("ScatterND", "none"),
            ("ScatterND", "add"),
            ("ScatterND", "mul"),
        ]
    )
    def test_scatter_18_17_allowed_reductions_success(
        self, op_name: str, reduction: str | None
    ) -> None:
        graph = self._make_scatter_graph(op_name, TensorProto.FLOAT, reduction)
        converted = self._converted(graph, helper.make_operatorsetid("", 18), 17)
        checker.check_model(converted)

    # raw_data INT64 initializers + dims/raw byte-length mismatch guard.

    def test_split_13_12_raw_data_initializer(self) -> None:
        split_init = helper.make_tensor(
            "split", TensorProto.INT64, [2], np.array([1, 3], dtype=np.int64), raw=True
        )
        graph = helper.make_graph(
            [helper.make_node("Split", ["X", "split"], ["Y0", "Y1"], axis=0)],
            "g",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (4, 2))],
            [
                helper.make_tensor_value_info("Y0", TensorProto.FLOAT, (1, 2)),
                helper.make_tensor_value_info("Y1", TensorProto.FLOAT, (3, 2)),
            ],
            initializer=[split_init],
        )
        converted = self._converted(graph, helper.make_operatorsetid("", 13), 12)
        attr = next(a for a in converted.graph.node[0].attribute if a.name == "split")
        assert list(attr.ints) == [1, 3]

    def test_reshape_5_4_raw_data_initializer(self) -> None:
        shape_init = helper.make_tensor(
            "shape", TensorProto.INT64, [2], np.array([2, 5], dtype=np.int64), raw=True
        )
        graph = helper.make_graph(
            [helper.make_node("Reshape", ["X", "shape"], ["Y"])],
            "g",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (10,))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (2, 5))],
            initializer=[shape_init],
        )
        converted = self._converted(graph, helper.make_operatorsetid("", 5), 4)
        reshape = next(n for n in converted.graph.node if n.op_type == "Reshape")
        attr = next(a for a in reshape.attribute if a.name == "shape")
        assert list(attr.ints) == [2, 5]

    def test_axes_input_to_attr_13_12_raw_data(self) -> None:
        axes_init = helper.make_tensor(
            "axes", TensorProto.INT64, [1], np.array([0], dtype=np.int64), raw=True
        )
        graph = helper.make_graph(
            [helper.make_node("ReduceSum", ["X", "axes"], ["Y"])],
            "g",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (5, 5))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (1, 5))],
            initializer=[axes_init],
        )
        converted = self._converted(graph, helper.make_operatorsetid("", 13), 12)
        attr = next(a for a in converted.graph.node[0].attribute if a.name == "axes")
        assert list(attr.ints) == [0]

    def test_dft_20_19_raw_data_axis(self) -> None:
        axis_init = helper.make_tensor(
            "axis", TensorProto.INT64, [], np.array([1], dtype=np.int64), raw=True
        )
        graph = helper.make_graph(
            [helper.make_node("DFT", ["X", "", "axis"], ["Y"])],
            "g",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (1, 8, 2))],
            [helper.make_tensor_value_info("Y", TensorProto.FLOAT, (1, 8, 2))],
            initializer=[axis_init],
        )
        converted = self._converted(graph, helper.make_operatorsetid("", 20), 19)
        attr = next(a for a in converted.graph.node[0].attribute if a.name == "axis")
        assert attr.i == 1

    def test_split_13_12_raw_data_dims_mismatch_rejected(self) -> None:
        # dims=[2] vs 8-byte raw_data fed via a Constant node; old code OOB-read.
        split_value = TensorProto(
            data_type=TensorProto.INT64,
            dims=[2],
            raw_data=struct.pack("<q", 1),
        )
        graph = helper.make_graph(
            [
                helper.make_node("Constant", [], ["split"], value=split_value),
                helper.make_node("Split", ["X", "split"], ["Y0", "Y1"], axis=0),
            ],
            "g",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (4, 2))],
            [
                helper.make_tensor_value_info("Y0", TensorProto.FLOAT, (1, 2)),
                helper.make_tensor_value_info("Y1", TensorProto.FLOAT, (3, 2)),
            ],
        )
        # Bypass checker to exercise the converter guard directly.
        model = helper.make_model(
            graph, opset_imports=[helper.make_operatorsetid("", 13)]
        )
        with self.assertRaises(RuntimeError):
            onnx.version_converter.convert_version(model, 12)

    def test_split_13_12_raw_data_constant_node(self) -> None:
        split_value = helper.make_tensor(
            "", TensorProto.INT64, [2], np.array([1, 3], dtype=np.int64), raw=True
        )
        graph = helper.make_graph(
            [
                helper.make_node("Constant", [], ["split"], value=split_value),
                helper.make_node("Split", ["X", "split"], ["Y0", "Y1"], axis=0),
            ],
            "g",
            [helper.make_tensor_value_info("X", TensorProto.FLOAT, (4, 2))],
            [
                helper.make_tensor_value_info("Y0", TensorProto.FLOAT, (1, 2)),
                helper.make_tensor_value_info("Y1", TensorProto.FLOAT, (3, 2)),
            ],
        )
        converted = self._converted(graph, helper.make_operatorsetid("", 13), 12)
        split = next(n for n in converted.graph.node if n.op_type == "Split")
        attr = next(a for a in split.attribute if a.name == "split")
        assert list(attr.ints) == [1, 3]

    # Range 26 -> 27: CompatibleAdapter (forward upgrade, no attribute changes)
    def test_range_26_27_float_success(self) -> None:
        nodes = [helper.make_node("Range", ["start", "limit", "delta"], ["output"])]
        graph = helper.make_graph(
            nodes,
            "range_26_27",
            [
                helper.make_tensor_value_info("start", TensorProto.FLOAT, []),
                helper.make_tensor_value_info("limit", TensorProto.FLOAT, []),
                helper.make_tensor_value_info("delta", TensorProto.FLOAT, []),
            ],
            [helper.make_tensor_value_info("output", TensorProto.FLOAT, None)],
        )
        converted = self._converted(graph, helper.make_operatorsetid("", 26), 27)
        assert converted.opset_import[0].version == 27

    # Range 27 -> 26: stash_type attribute must be removed on downgrade
    def test_range_27_26_stash_type_removed(self) -> None:
        nodes = [
            helper.make_node(
                "Range", ["start", "limit", "delta"], ["output"], stash_type=1
            )
        ]
        graph = helper.make_graph(
            nodes,
            "range_27_26",
            [
                helper.make_tensor_value_info("start", TensorProto.FLOAT, []),
                helper.make_tensor_value_info("limit", TensorProto.FLOAT, []),
                helper.make_tensor_value_info("delta", TensorProto.FLOAT, []),
            ],
            [helper.make_tensor_value_info("output", TensorProto.FLOAT, None)],
        )
        converted = self._converted(graph, helper.make_operatorsetid("", 27), 26)
        assert converted.opset_import[0].version == 26
        range_node = next(n for n in converted.graph.node if n.op_type == "Range")
        assert not any(a.name == "stash_type" for a in range_node.attribute)

    # Range 27 -> 26: float16 and bfloat16 inputs must be rejected
    @parameterized.parameterized.expand(
        [
            ("float16", TensorProto.FLOAT16),
            ("bfloat16", TensorProto.BFLOAT16),
        ]
    )
    def test_range_27_26_low_precision_fails(self, _: str, dtype: int) -> None:
        def test() -> None:
            nodes = [helper.make_node("Range", ["start", "limit", "delta"], ["output"])]
            graph = helper.make_graph(
                nodes,
                "range_27_26_fp16",
                [
                    helper.make_tensor_value_info("start", dtype, []),
                    helper.make_tensor_value_info("limit", dtype, []),
                    helper.make_tensor_value_info("delta", dtype, []),
                ],
                [helper.make_tensor_value_info("output", dtype, None)],
            )
            self._converted(graph, helper.make_operatorsetid("", 27), 26)

        self.assertRaises(RuntimeError, test)


if __name__ == "__main__":
    unittest.main()
