File size: 5,914 Bytes
9422383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "arm_yolo26n.h"

enum {
  // Little-endian model.bin header and first-directory-entry fields.
  kResolutionOffset = 12,
  kClassCountOffset = 16,
  kSlotCountOffset = 20,
  kFirstSlotSizeOffset = 72,
};

static const uint32_t kUnrepresentableClassCount = UINT32_C(2147483648);

/*
 * The Python API validates tensor sizes and allocates disjoint input and output
 * buffers before calling the native runtime. Its end-to-end tests therefore
 * cannot reach the C API's own rejection paths. This test calls that public C
 * boundary directly and verifies invalid buffers are rejected without starting
 * an inference or changing the observable execution count.
 */

static void fail(const char* message) {
  fprintf(stderr, "%s\n", message);
  exit(EXIT_FAILURE);
}

static unsigned char* read_model(const char* path, size_t* size) {
  FILE* file = fopen(path, "rb");
  if (file == NULL || fseek(file, 0, SEEK_END) != 0) fail("could not open test model");
  const long end = ftell(file);
  if (end <= 0 || fseek(file, 0, SEEK_SET) != 0) fail("could not size test model");

  *size = (size_t)end;
  unsigned char* data = malloc(*size);
  if (data == NULL || fread(data, 1, *size, file) != *size) fail("could not read test model");
  if (fclose(file) != 0) fail("could not close test model");
  return data;
}

static void write_u32(unsigned char* data, size_t offset, uint32_t value) {
  memcpy(data + offset, &value, sizeof(value));
}

static void require_model_rejected(unsigned char* data, size_t size, const char* message) {
  if (y26_native_create(data, size) != NULL) fail(message);
}

int main(void) {
  const char* model_path = getenv("YOLO26N_RUNTIME_API_TEST_MODEL");
  if (model_path == NULL || model_path[0] == '\0') {
    printf("YOLO26N_RUNTIME_API_TEST_MODEL is not set\n");
    return 77;
  }

  // Setup: create the runtime from caller-owned bytes through the public C API.
  size_t model_size = 0;
  unsigned char* model_data = read_model(model_path, &model_size);
  if (y26_native_create(NULL, model_size) != NULL || y26_native_create(model_data, 0) != NULL) {
    fail("invalid model span was not rejected");
  }

  // These fields control graph shapes and weight lookup. Each unsupported
  // value must fail at the public creation boundary, before inference is
  // possible. Restore the valid bytes after every independent case.
  unsigned char original[4];
  memcpy(original, model_data + kResolutionOffset, sizeof(original));
  write_u32(model_data, kResolutionOffset, 321);
  require_model_rejected(model_data, model_size, "unsupported resolution was not rejected");
  memcpy(model_data + kResolutionOffset, original, sizeof(original));

  memcpy(original, model_data + kClassCountOffset, sizeof(original));
  write_u32(model_data, kClassCountOffset, 0);
  require_model_rejected(model_data, model_size, "zero class count was not rejected");
  write_u32(model_data, kClassCountOffset, kUnrepresentableClassCount);
  require_model_rejected(model_data, model_size,
                         "unrepresentable model dimensions were not rejected");
  memcpy(model_data + kClassCountOffset, original, sizeof(original));

  memcpy(original, model_data + kSlotCountOffset, sizeof(original));
  uint32_t slot_count = 0;
  memcpy(&slot_count, original, sizeof(slot_count));
  if (slot_count <= 1) fail("test model has an unexpectedly small slot directory");
  write_u32(model_data, kSlotCountOffset, slot_count - 1);
  require_model_rejected(model_data, model_size, "incomplete weight directory was not rejected");
  memcpy(model_data + kSlotCountOffset, original, sizeof(original));

  // Changing a slot extent without changing its payload range leaves a
  // structurally readable file, but it no longer matches the fixed graph.
  unsigned char original_size[8];
  memcpy(original_size, model_data + kFirstSlotSizeOffset, sizeof(original_size));
  uint64_t first_slot_size = 0;
  memcpy(&first_slot_size, original_size, sizeof(first_slot_size));
  if (first_slot_size <= 1) fail("test model has an unexpectedly small first weight");
  --first_slot_size;
  memcpy(model_data + kFirstSlotSizeOffset, &first_slot_size, sizeof(first_slot_size));
  require_model_rejected(model_data, model_size, "incorrect weight extent was not rejected");
  memcpy(model_data + kFirstSlotSizeOffset, original_size, sizeof(original_size));

  y26_native_runtime* runtime = y26_native_create(model_data, model_size);
  if (runtime == NULL) fail("could not create runtime from model");
  free(model_data);

  const size_t required_input_bytes = y26_native_input_bytes(runtime);
  const size_t required_output_bytes = y26_native_output_bytes(runtime);
  const size_t shared_bytes =
      required_input_bytes > required_output_bytes ? required_input_bytes : required_output_bytes;
  float* input = malloc(required_input_bytes);
  float* output = malloc(required_output_bytes);
  float* shared = malloc(shared_bytes);
  if (input == NULL || output == NULL || shared == NULL) fail("could not allocate test buffers");

  // Exercise the two invalid-buffer cases without performing inference.
  const y26_native_status wrong_size = y26_native_execute(
      runtime, input, required_input_bytes - sizeof(float), output, required_output_bytes);
  const y26_native_status overlap =
      y26_native_execute(runtime, shared, required_input_bytes, shared, required_output_bytes);

  // Verify both calls were rejected before changing observable runtime state.
  if (wrong_size != Y26_NATIVE_INVALID_ARGUMENT) fail("incorrect input size was not rejected");
  if (overlap != Y26_NATIVE_INVALID_ARGUMENT) fail("overlapping buffers were not rejected");
  if (y26_native_calls(runtime) != 0) fail("rejected calls changed the execution count");

  free(shared);
  free(output);
  free(input);
  y26_native_destroy(runtime);
  return EXIT_SUCCESS;
}