Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  browser_ml_cache.js

  Sprache: JAVA
 

/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

const { sinon } = ChromeUtils.importESModule(
  "resource://testing-common/Sinon.sys.mjs"
);

const { ProgressStatusText, ProgressType } = ChromeUtils.importESModule(
  "chrome://global/content/ml/Utils.sys.mjs"
);
const { OPFS } = ChromeUtils.importESModule(
  "chrome://global/content/ml/OPFS.sys.mjs"
);

const { URLChecker } = ChromeUtils.importESModule(
  "chrome://global/content/ml/Utils.sys.mjs"
);

const { Progress, MLUtils } = ChromeUtils.importESModule(
  "chrome://global/content/ml/Utils.sys.mjs"
);

// Root URL of the fake hub, see the `data` dir in the tests.
const FAKE_HUB =
  "chrome://mochitests/content/browser/toolkit/components/ml/tests/browser/data";
const FAKE_URL_TEMPLATE = "{model}/resolve/{revision}";

const FAKE_MODEL_ARGS = {
  model: "acme/bert",
  revision: "main",
  file: "config.json",
  taskName: "task_model",
};

const FAKE_RELEASED_MODEL_ARGS = {
  model: "acme/bert",
  revision: "v0.1",
  file: "config.json",
  taskName: "task_released",
};

const FAKE_ONNX_MODEL_ARGS = {
  model: "acme/bert",
  revision: "main",
  file: "onnx/config.json",
  taskName: "task_onnx",
};

/**
 * Test the MOZ_ALLOW_EXTERNAL_ML_HUB environment variable
 */

add_task(async function test_allow_external_ml_hub() {
  Services.env.set("MOZ_ALLOW_EXTERNAL_ML_HUB""1");
  new ModelHub({ rootUrl: "https://huggingface.co" });
  Services.env.set("MOZ_ALLOW_EXTERNAL_ML_HUB""");
});

const badInputs = [
  [
    {
      model: "ac me/bert",
      revision: "main",
      file: "config.json",
    },
    "Org can only contain letters, numbers, and hyphens",
  ],
  [
    {
      model: "1111/bert",
      revision: "main",
      file: "config.json",
    },
    "Org cannot contain only numbers",
  ],
  [
    {
      model: "-acme/bert",
      revision: "main",
      file: "config.json",
    },
    "Org start or end with a hyphen, or use consecutive hyphens",
  ],
  [
    {
      model: "a-c-m-e/#bert",
      revision: "main",
      file: "config.json",
    },
    "Models can only contain letters, numbers, and hyphens, underscord, periods",
  ],
  [
    {
      model: "a-c-m-e/b$ert",
      revision: "main",
      file: "config.json",
    },
    "Models cannot contain spaces or control characters",
  ],
  [
    {
      model: "a-c-m-e/b$ert",
      revision: "main",
      file: ".filename",
    },
    "File",
  ],
];

/**
 * Make sure we reject bad inputs.
 */

add_task(async function test_bad_inputs() {
  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
  });

  for (const badInput of badInputs) {
    const params = badInput[0];
    const errorMsg = badInput[1];
    try {
      await hub.getModelFileAsArrayBuffer(params);
    } catch (error) {
      continue;
    }
    throw new Error(errorMsg);
  }
});

/**
 * Test that we can retrieve a file as an ArrayBuffer.
 */

add_task(async function test_getting_file() {
  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
  });

  let [array, headers] = await hub.getModelFileAsArrayBuffer(FAKE_MODEL_ARGS);

  Assert.equal(headers["Content-Type"], "application/json");

  // check the content of the file.
  let jsonData = JSON.parse(
    String.fromCharCode.apply(nullnew Uint8Array(array))
  );

  Assert.equal(jsonData.hidden_size, 32);
});

/**
 * Test that we can retrieve a file as an ArrayBuffer even if we don't have headers
 */

add_task(async function test_getting_file_no_headers() {
  await SpecialPowers.pushPrefEnv({
    set: [
      // Enabled by default.
      ["browser.ml.logLevel""All"],
    ],
  });

  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
    reset: true,
  });

  // Return empty headers
  sinon.stub(hub, "extractHeaders").callsFake(function () {
    return {};
  });

  let [array, headers] = await hub.getModelFileAsArrayBuffer(FAKE_MODEL_ARGS);

  Assert.equal(headers["Content-Type"], "application/octet-stream"); // default content type

  // check the content of the file.
  let jsonData = JSON.parse(
    String.fromCharCode.apply(nullnew Uint8Array(array))
  );

  Assert.equal(jsonData.hidden_size, 32);

  hub.extractHeaders.restore();
  await deleteCache(hub.cache);
});

/**
 * Test that we can retrieve a file from a released model and skip head calls
 */

add_task(async function test_getting_released_file() {
  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
  });
  let spy = sinon.spy(hub, "getETag");
  let [array, headers] = await hub.getModelFileAsArrayBuffer(
    FAKE_RELEASED_MODEL_ARGS
  );

  Assert.equal(headers["Content-Type"], "application/json");

  // check the content of the file.
  let jsonData = JSON.parse(
    String.fromCharCode.apply(nullnew Uint8Array(array))
  );

  Assert.equal(jsonData.hidden_size, 768);

  // check that head calls were not made
  Assert.ok(!spy.called, "getETag should have never been called.");
  spy.restore();
});

/**
 * Make sure files can be located in sub directories
 */

add_task(async function test_getting_file_in_subdir() {
  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
  });

  let [array, metadata] =
    await hub.getModelFileAsArrayBuffer(FAKE_ONNX_MODEL_ARGS);

  Assert.equal(metadata["Content-Type"], "application/json");

  // check the content of the file.
  let jsonData = JSON.parse(
    String.fromCharCode.apply(nullnew Uint8Array(array))
  );

  Assert.equal(jsonData.hidden_size, 768);
});

/**
 * Test that we can use a custom URL template.
 */

add_task(async function test_getting_file_custom_path() {
  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: "{model}/resolve/{revision}",
  });

  let res = await hub.getModelFileAsArrayBuffer(FAKE_MODEL_ARGS);

  Assert.equal(res[1]["Content-Type"], "application/json");
});

/**
 * Test that we can't use an URL with a query for the template
 */

add_task(async function test_getting_file_custom_path_rogue() {
  const urlTemplate = "{model}/resolve/{revision}/?some_id=bedqwdw";
  Assert.throws(
    () => new ModelHub({ rootUrl: FAKE_HUB, urlTemplate }),
    /Invalid URL template/,
    `Should throw with ${urlTemplate}`
  );
});

/**
 * Test that the file can be returned as a response and its content correct.
 */

add_task(async function test_getting_file_as_response() {
  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
  });

  let response = await hub.getModelFileAsResponse(FAKE_MODEL_ARGS);

  // check the content of the file.
  let jsonData = await response.json();
  Assert.equal(jsonData.hidden_size, 32);
});

/**
 * Test that the cache is used when the data is retrieved from the server
 * and that the cache is updated with the new data.
 */

add_task(async function test_getting_file_from_cache() {
  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
  });
  let array = await hub.getModelFileAsArrayBuffer(FAKE_MODEL_ARGS);

  var lastUsed = array[1].lastUsed;

  // stub to verify that the data was retrieved from IndexDB
  let matchMethod = hub.cache._testGetData;

  sinon.stub(hub.cache, "_testGetData").callsFake(function () {
    return matchMethod.apply(this, arguments).then(result => {
      Assert.notEqual(result, null);
      return result;
    });
  });

  // exercises the cache
  let array2 = await hub.getModelFileAsArrayBuffer(FAKE_MODEL_ARGS);
  hub.cache._testGetData.restore();

  let newLastUsed = array2[1].lastUsed;

  // make sure the last used field was updated
  Assert.greater(newLastUsed, lastUsed);

  // we don't compare the lastUsed fiel because it changes for each read
  Assert.deepEqual(stripLastUsed(array), stripLastUsed(array2));
});

/**
 * Test that the callback is appropriately called when the data is retrieved from the server
 * or from the cache.
 */

add_task(async function test_getting_file_from_url_cache_with_callback() {
  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
  });

  hub.cache = await initializeCache();

  let numCalls = 0;
  let currentData = null;
  let array = await hub.getModelFileAsArrayBuffer({
    ...FAKE_MODEL_ARGS,
    progressCallback: data => {
      // expecting initiate status and download
      currentData = data;
      if (numCalls == 0) {
        Assert.deepEqual(
          {
            type: data.type,
            statusText: data.statusText,
            ok: data.ok,
            model: currentData?.metadata?.model,
            file: currentData?.metadata?.file,
            revision: currentData?.metadata?.revision,
            taskName: currentData?.metadata?.taskName,
          },
          {
            type: ProgressType.DOWNLOAD,
            statusText: ProgressStatusText.INITIATE,
            ok: true,
            ...FAKE_MODEL_ARGS,
          },
          "Initiate Data from server should be correct"
        );
      }

      if (numCalls == 1) {
        Assert.deepEqual(
          {
            type: data.type,
            statusText: data.statusText,
            ok: data.ok,
            model: currentData?.metadata?.model,
            file: currentData?.metadata?.file,
            revision: currentData?.metadata?.revision,
            taskName: currentData?.metadata?.taskName,
          },
          {
            type: ProgressType.DOWNLOAD,
            statusText: ProgressStatusText.SIZE_ESTIMATE,
            ok: true,
            ...FAKE_MODEL_ARGS,
          },
          "size estimate Data from server should be correct"
        );
      }

      numCalls += 1;
    },
  });

  var lastUsed = array[1].lastUsed;

  Assert.greaterOrEqual(numCalls, 3);

  // last received message is DONE
  Assert.deepEqual(
    {
      type: currentData?.type,
      statusText: currentData?.statusText,
      ok: currentData?.ok,
      model: currentData?.metadata?.model,
      file: currentData?.metadata?.file,
      revision: currentData?.metadata?.revision,
      taskName: currentData?.metadata?.taskName,
    },
    {
      type: ProgressType.DOWNLOAD,
      statusText: ProgressStatusText.DONE,
      ok: true,
      ...FAKE_MODEL_ARGS,
    },
    "Done Data from server should be correct"
  );

  // stub to verify that the data was retrieved from IndexDB
  let matchMethod = hub.cache._testGetData;

  sinon.stub(hub.cache, "_testGetData").callsFake(function () {
    return matchMethod.apply(this, arguments).then(result => {
      Assert.notEqual(result, null);
      return result;
    });
  });

  numCalls = 0;
  currentData = null;

  // Now we expect the callback to indicate cache usage.
  let array2 = await hub.getModelFileAsArrayBuffer({
    ...FAKE_MODEL_ARGS,
    progressCallback: data => {
      // expecting initiate status and download

      currentData = data;

      if (numCalls == 0) {
        Assert.deepEqual(
          {
            type: data.type,
            statusText: data.statusText,
            ok: data.ok,
            model: currentData?.metadata?.model,
            file: currentData?.metadata?.file,
            revision: currentData?.metadata?.revision,
            taskName: currentData?.metadata?.taskName,
          },
          {
            type: ProgressType.LOAD_FROM_CACHE,
            statusText: ProgressStatusText.INITIATE,
            ok: true,
            ...FAKE_MODEL_ARGS,
          },
          "Initiate Data from cache should be correct"
        );
      }

      numCalls += 1;
    },
  });
  hub.cache._testGetData.restore();

  let newLastUsed = array2[1].lastUsed;

  // make sure the last used field was updated
  Assert.greater(newLastUsed, lastUsed);

  Assert.deepEqual(stripLastUsed(array), stripLastUsed(array2));

  // last received message is DONE
  Assert.deepEqual(
    {
      type: currentData?.type,
      statusText: currentData?.statusText,
      ok: currentData?.ok,
      model: currentData?.metadata?.model,
      file: currentData?.metadata?.file,
      revision: currentData?.metadata?.revision,
      taskName: currentData?.metadata?.taskName,
    },
    {
      type: ProgressType.LOAD_FROM_CACHE,
      statusText: ProgressStatusText.DONE,
      ok: true,
      ...FAKE_MODEL_ARGS,
    },
    "Done Data from cache should be correct"
  );

  await deleteCache(hub.cache);
});

/**
 * Test parsing of a well-formed full URL, including protocol and path.
 */

add_task(async function testWellFormedFullUrl() {
  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: "{model}/{revision}",
  });
  const url = `${FAKE_HUB}/org1/model1/v1/file/path`;
  const result = hub.parseUrl(url);

  Assert.equal(
    result.model,
    "org1/model1",
    "Model should be parsed correctly."
  );
  Assert.equal(result.revision, "v1""Revision should be parsed correctly.");
  Assert.equal(
    result.file,
    "file/path",
    "File path should be parsed correctly."
  );
});

/**
 * Test parsing of well-formed URLs, starting with a slash.
 */

const URLS_AND_RESULT = [
  {
    url: "/Xenova/bert-base-NER/resolve/main/onnx/model.onnx",
    model: "Xenova/bert-base-NER",
    revision: "main",
    file: "onnx/model.onnx",
    urlTemplate: "{model}/resolve/{revision}",
  },
  {
    url: "/org1/model1/v1/file/path",
    model: "org1/model1",
    revision: "v1",
    file: "file/path",
    urlTemplate: "{model}/{revision}",
  },
];

add_task(async function testWellFormedRelativeUrl() {
  for (const example of URLS_AND_RESULT) {
    const hub = new ModelHub({
      rootUrl: FAKE_HUB,
      urlTemplate: example.urlTemplate,
    });
    const result = hub.parseUrl(example.url);

    Assert.equal(
      result.model,
      example.model,
      "Model should be parsed correctly."
    );
    Assert.equal(
      result.revision,
      example.revision,
      "Revision should be parsed correctly."
    );
    Assert.equal(
      result.file,
      example.file,
      "File path should be parsed correctly."
    );
  }
});

/**
 * Ensures an error is thrown when the URL does not start with the expected root URL or a slash.
 */

add_task(async function testInvalidDomain() {
  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
  });
  const url = "https://example.com/org1/model1/resolve/v1/file/path";
  Assert.throws(
    () => hub.parseUrl(url),
    new RegExp(`Error: Invalid domain for model URL: ${url}`),
    `Should throw with ${url}`
  );
});

/**
 * Tests the method's error handling when the URL format does not include the required segments.
 */

add_task(async function testTooFewParts() {
  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
  });
  const url = "/org1/model1/resolve";
  Assert.throws(
    () => hub.parseUrl(url),
    new RegExp(`Error: Invalid model URL format: ${url}`),
    `Should throw with ${url}`
  );
});

// IndexedDB tests

/**
 * Test the initialization and creation of the IndexedDBCache instance.
 */

add_task(async function test_Init() {
  const cache = await initializeCache();
  Assert.ok(
    cache instanceof IndexedDBCache,
    "The cache instance should be created successfully."
  );
  Assert.ok(
    IDBDatabase.isInstance(cache.db),
    `The cache should have an IDBDatabase instance. Found ${cache.db}`
  );
  await deleteCache(cache);
});

/**
 * Test checking existence of data in the cache.
 */

add_task(async function test_PutAndCheckExists() {
  const cache = await initializeCache();
  const testData = createBlob();
  const key = "file.txt";
  await cache.put({
    taskName: "task",
    model: "org/model",
    revision: "v1",
    file: "file.txt",
    data: testData,
    headers: {
      ETag: "ETAG123",
    },
  });

  // Checking if the file exists
  let exists = await cache.fileExists({
    model: "org/model",
    revision: "v1",
    file: key,
  });
  Assert.ok(exists, "The file should exist in the cache.");

  // Removing all files from the model
  await cache.deleteModels({ model: "org/model", revision: "v1" });

  exists = await cache.fileExists({
    taskName: "task",
    model: "org/model",
    revision: "v1",
    file: key,
  });
  Assert.ok(!exists, "The file should be gone from the cache.");

  await deleteCache(cache);
});

/**
 * Test adding data to the cache and retrieving it.
 */

add_task(async function test_PutAndGet() {
  const cache = await initializeCache();
  const testData = createBlob();
  await cache.put({
    taskName: "task",
    model: "org/model",
    revision: "v1",
    file: "file.txt",
    data: testData,
    headers: {
      ETag: "ETAG123",
    },
  });

  const [retrievedData, headers] = await cache.getFile({
    model: "org/model",
    revision: "v1",
    file: "file.txt",
  });
  Assert.deepEqual(
    retrievedData,
    testData,
    "The retrieved data should match the stored data."
  );
  Assert.equal(
    headers.ETag,
    "ETAG123",
    "The retrieved ETag should match the stored ETag."
  );

  await deleteCache(cache);
});

/**
 * Test retrieving the headers for a cache entry.
 */

add_task(async function test_GetHeaders() {
  const cache = await initializeCache();
  const testData = createBlob();
  const headers = {
    ETag: "ETAG123",
    status: 200,
    extra: "extra",
  };

  const when = await cache.put({
    taskName: "task",
    model: "org/model",
    revision: "v1",
    file: "file.txt",
    data: testData,
    headers,
  });

  const storedHeaders = await cache.getHeaders({
    model: "org/model",
    revision: "v1",
    file: "file.txt",
  });

  // The `extra` field should be removed from the stored headers because
  // it's not part of the allowed keys.
  // The content-type one is added when not present
  Assert.deepEqual(
    {
      ETag: "ETAG123",
      status: 200,
      "Content-Type""application/octet-stream",
      fileSize: 8,
      lastUsed: when,
      lastUpdated: when,
    },
    storedHeaders,
    "The retrieved headers should match the stored headers."
  );
  await deleteCache(cache);
});

/**
 * Test listing all models stored in the cache.
 */

add_task(async function test_ListModels() {
  const cache = await initializeCache();

  await Promise.all([
    cache.put({
      taskName: "task1",
      model: "org1/modelA",
      revision: "v1",
      file: "file1.txt",
      data: createBlob(),
      headers: null,
    }),
    cache.put({
      taskName: "task2",
      model: "org2/modelB",
      revision: "v2",
      file: "file2.txt",
      data: createBlob(),
      headers: null,
    }),
  ]);

  const models = await cache.listModels();
  const expected = [
    { name: "org1/modelA", revision: "v1", taskName: "task1" },
    { name: "org2/modelB", revision: "v2", taskName: "task2" },
  ];
  Assert.deepEqual(models, expected, "All models should be listed");
  await deleteCache(cache);
});

/**
 * Test deleting a model and its data from the cache.
 */

add_task(async function test_DeleteModels() {
  const cache = await initializeCache();
  await cache.put({
    taskName: "task",
    model: "org/model",
    revision: "v1",
    file: "file.txt",
    data: createBlob(),
    headers: null,
  });
  await cache.deleteModels({ model: "org/model", revision: "v1" });

  const dataAfterDelete = await cache.getFile({
    model: "org/model",
    revision: "v1",
    file: "file.txt",
  });
  Assert.equal(
    dataAfterDelete,
    null,
    "The data for the deleted model should not exist."
  );
  await deleteCache(cache);
});

/**
 * Test that after deleting a model from the cache, the remaing models are still there.
 */

add_task(async function test_nonDeletedModels() {
  const cache = await initializeCache();

  const testData = createRandomBlob();

  await Promise.all([
    cache.put({
      taskName: "task1",
      model: "org/model",
      revision: "v1",
      file: "file.txt",
      data: testData,
      headers: {
        ETag: "ETAG123",
      },
    }),
    cache.put({
      taskName: "task2",
      model: "org/model2",
      revision: "v1",
      file: "file.txt",
      data: createRandomBlob(),
      headers: {
        ETag: "ETAG1234",
      },
    }),

    cache.put({
      taskName: "task3",
      model: "org/model2",
      revision: "v1",
      file: "file2.txt",
      data: createRandomBlob(),
      headers: {
        ETag: "ETAG1234",
      },
    }),
  ]);

  await cache.deleteModels({ model: "org/model2", revision: "v1" });

  const [retrievedData, headers] = await cache.getFile({
    model: "org/model",
    revision: "v1",
    file: "file.txt",
  });
  Assert.deepEqual(
    retrievedData,
    testData,
    "The retrieved data should match the stored data."
  );

  Assert.equal(
    headers.ETag,
    "ETAG123",
    "The retrieved ETag should match the stored ETag."
  );

  const dataAfterDelete = await cache.getFile({
    model: "org/model2",
    revision: "v1",
    file: "file.txt",
  });
  Assert.equal(
    dataAfterDelete,
    null,
    "The data for the deleted model should not exist."
  );

  const dataAfterDelete2 = await cache.getFile({
    model: "org/model2",
    revision: "v1",
    file: "file2.txt",
  });
  Assert.equal(
    dataAfterDelete2,
    null,
    "The data for the deleted model should not exist."
  );

  await deleteCache(cache);
});

/**
 * Test deleting a model and its data from the cache using a task name.
 */

add_task(async function test_DeleteModelsUsingTaskName() {
  const cache = await initializeCache();
  const model = "mozilla/distilvit";
  const revision = "main";
  const taskName = "echo";

  await cache.put({
    taskName,
    model,
    revision,
    file: "file.txt",
    data: createBlob(),
    headers: null,
  });

  await cache.deleteModels({ taskName });

  // Model should be gone.
  const models = await cache.listModels();
  const expected = [];
  Assert.deepEqual(models, expected, "All models should be deleted.");

  const dataAfterDelete = await cache.getFile({
    model,
    revision,
    file: "file.txt",
  });
  Assert.equal(
    dataAfterDelete,
    null,
    "The data for the deleted model should not exist."
  );
  await deleteCache(cache);
});

/**
 * Test deleting a model and its data from the cache using a non-existing task name.
 */

add_task(async function test_DeleteModelsUsingNonExistingTaskName() {
  const cache = await initializeCache();
  const model = "mozilla/distilvit";
  const revision = "main";
  const taskName = "echo";

  await cache.put({
    taskName,
    model,
    revision,
    file: "file.txt",
    data: createBlob(),
    headers: null,
  });

  await cache.deleteModels({ taskName: "non-existing-task" });

  // Model should still be there.
  const models = await cache.listModels();
  const expected = [{ name: model, revision, taskName }];
  Assert.deepEqual(models, expected, "All models should be listed");

  await deleteCache(cache);
});

/**
 * Test that after deleting a model from the cache, the remaing models are still there.
 */

add_task(async function test_deleteNonMatchingModelRevisions() {
  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
  });

  const cache = await initializeCache();

  hub.cache = cache;

  const testData = createRandomBlob();

  const testData2 = createRandomBlob();

  const taskName = "task";

  const file = "file.txt";
  const hostname = new URL(FAKE_HUB).hostname;

  await Promise.all([
    cache.put({
      taskName,
      model: `${hostname}/org/model`,
      revision: "v1",
      file,
      data: testData,
      headers: {
        ETag: "ETAG123",
      },
    }),
    cache.put({
      taskName,
      model: `${hostname}/org/model2`,
      revision: "v1",
      file,
      data: createRandomBlob(),
      headers: {
        ETag: "ETAG1234",
      },
    }),

    cache.put({
      taskName,
      model: `${hostname}/org/model2`,
      revision: "v2",
      file,
      data: createRandomBlob(),
      headers: {
        ETag: "ETAG1234",
      },
    }),

    cache.put({
      taskName,
      model: `${hostname}/org/model2`,
      revision: "v3",
      file,
      data: testData2,
      headers: {
        ETag: "ETAG1234",
      },
    }),
  ]);

  await hub.deleteNonMatchingModelRevisions(
    taskName,
    `${hostname}/org/model2`,
    "v3"
  );

  const [retrievedData, headers] = await cache.getFile({
    model: `${hostname}/org/model`,
    revision: "v1",
    file,
  });
  Assert.deepEqual(
    retrievedData,
    testData,
    "The retrieved data should match the stored data."
  );
  Assert.equal(
    headers.ETag,
    "ETAG123",
    "The retrieved ETag should match the stored ETag."
  );

  const dataAfterDelete = await cache.getFile({
    model: `${hostname}/org/model2`,
    revision: "v1",
    file,
  });
  Assert.equal(dataAfterDelete, null"The data for v1 should not exist.");

  const dataAfterDelete2 = await cache.getFile({
    model: `${hostname}/org/model2`,
    revision: "v2",
    file,
  });
  Assert.equal(dataAfterDelete2, null"The data for v2 should not exist.");

  const [retrievedData2, headers2] = await cache.getFile({
    model: `${hostname}/org/model2`,
    revision: "v3",
    file,
  });
  Assert.deepEqual(
    retrievedData2,
    testData2,
    "The retrieved data for v3 should match the stored data."
  );

  Assert.equal(
    headers2.ETag,
    "ETAG1234",
    "The retrieved ETag for v3 should match the stored ETag."
  );

  await deleteCache(cache);
});

/**
 * Test listing files
 */

add_task(async function test_listFiles() {
  const cache = await initializeCache();
  const headers = { "Content-Length""12345", ETag: "XYZ" };
  const blob = createBlob();

  const when1 = await cache.put({
    taskName: "task1",
    model: "org/model",
    revision: "v1",
    file: "file.txt",
    data: blob,
    headers: null,
  });

  const when2 = await cache.put({
    taskName: "task1",
    model: "org/model",
    revision: "v1",
    file: "file2.txt",
    data: blob,
    headers: null,
  });

  const when3 = await cache.put({
    taskName: "task2",
    model: "org/model",
    revision: "v1",
    file: "sub/file3.txt",
    data: createBlob(32),
    headers,
  });

  const { files } = await cache.listFiles({
    model: "org/model",
    revision: "v1",
  });
  const expected = [
    {
      path: "file.txt",
      headers: {
        "Content-Type""application/octet-stream",
        fileSize: 8,
        ETag: "NO_ETAG",
        lastUsed: when1,
        lastUpdated: when1,
      },
      engineIds: [],
    },
    {
      path: "file2.txt",
      headers: {
        "Content-Type""application/octet-stream",
        fileSize: 8,
        ETag: "NO_ETAG",
        lastUsed: when2,
        lastUpdated: when2,
      },
      engineIds: [],
    },
    {
      path: "sub/file3.txt",
      headers: {
        "Content-Length""12345",
        "Content-Type""application/octet-stream",
        fileSize: 32,
        ETag: "XYZ",
        lastUsed: when3,
        lastUpdated: when3,
      },
      engineIds: [],
    },
  ];

  Assert.deepEqual(files, expected);
  await deleteCache(cache);
});

/**
 * Test listing files using a task name
 */

add_task(async function test_listFilesUsingTaskName() {
  const cache = await initializeCache();

  const model = "mozilla/distilvit";
  const revision = "main";
  const taskName = "echo";

  const headers = { "Content-Length""12345", ETag: "XYZ" };
  const blob = createBlob();

  const when1 = await cache.put({
    taskName,
    model,
    revision,
    file: "file.txt",
    data: blob,
    headers: null,
  });

  const when2 = await cache.put({
    taskName,
    model,
    revision,
    file: "file2.txt",
    data: blob,
    headers: null,
  });

  const when3 = await cache.put({
    taskName,
    model,
    revision,
    file: "sub/file3.txt",
    data: createBlob(32),
    headers,
  });

  const { files } = await cache.listFiles({ taskName, model, revision });
  const expected = [
    {
      path: "file.txt",
      headers: {
        "Content-Type""application/octet-stream",
        fileSize: 8,
        ETag: "NO_ETAG",
        lastUsed: when1,
        lastUpdated: when1,
      },
      engineIds: [],
    },
    {
      path: "file2.txt",
      headers: {
        "Content-Type""application/octet-stream",
        fileSize: 8,
        ETag: "NO_ETAG",
        lastUsed: when2,
        lastUpdated: when2,
      },
      engineIds: [],
    },
    {
      path: "sub/file3.txt",
      headers: {
        "Content-Length""12345",
        "Content-Type""application/octet-stream",
        fileSize: 32,
        ETag: "XYZ",
        lastUsed: when3,
        lastUpdated: when3,
      },
      engineIds: [],
    },
  ];

  Assert.deepEqual(files, expected);

  await deleteCache(cache);
});

/**
 * Test listing files using a non existing task name
 */

add_task(async function test_listFilesUsingNonExistingTaskName() {
  const cache = await initializeCache();

  const model = "mozilla/distilvit";
  const revision = "main";
  const taskName = "echo";

  const headers = { "Content-Length""12345", ETag: "XYZ" };
  const blob = createBlob();

  await Promise.all([
    cache.put({
      taskName,
      model,
      revision,
      file: "file.txt",
      data: blob,
      headers: null,
    }),
    cache.put({
      taskName,
      model,
      revision,
      file: "file2.txt",
      data: blob,
      headers: null,
    }),
    cache.put({
      taskName,
      model,
      revision,
      file: "sub/file3.txt",
      data: createBlob(32),
      headers,
    }),
  ]);

  const { files } = await cache.listFiles({ taskName: "non-existing-task" });

  Assert.deepEqual(files, []);

  await deleteCache(cache);
});

/**
 * Test the ability to add a database from a non-existing database.
 */

add_task(async function test_initDbFromNonExisting() {
  const cache = await initializeCache();

  Assert.notEqual(cache, null);

  await deleteCache(cache);
});

/**
 * Test that we can upgrade even if the existing database is missing some stores or indices.
 */

add_task(async function test_initDbFromExistingEmpty() {
  const dbName = `modelFiles-${crypto.randomUUID()}`;
  await TestIndexedDBCache.deleteDatabaseAndWait(dbName).catch(() => {});

  const dbVersion = 1;

  const newVersion = dbVersion + 1;

  async function openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(dbName, dbVersion);
      request.onerror = event => reject(event.target.error);
      request.onsuccess = event => resolve(event.target.result);
    });
  }

  const db = await openDB();
  db.close();

  const cache = await IndexedDBCache.init({ dbName, version: newVersion });

  Assert.notEqual(cache, null);
  Assert.equal(cache.db.version, newVersion);

  const model = "mozilla/distilvit";
  const revision = "main";
  const taskName = "echo";

  const blob = createBlob();

  const when = await cache.put({
    taskName,
    model,
    revision,
    file: "file.txt",
    data: blob,
    headers: null,
  });

  const expected = [
    {
      path: "file.txt",
      headers: {
        "Content-Type""application/octet-stream",
        fileSize: 8,
        ETag: "NO_ETAG",
        lastUsed: when,
        lastUpdated: when,
      },
      engineIds: [],
    },
  ];

  // Ensure every table & indices is on so that we can list files
  const { files } = await cache.listFiles({ taskName, model, revision });
  Assert.deepEqual(files, expected);

  await deleteCache(cache);
});

/**
 * Test that upgrading from version 1 to version 2 results in existing data being deleted.
 */

add_task(async function test_initDbFromExistingNoChange() {
  const dbName = `modelFiles-${crypto.randomUUID()}`;

  await TestIndexedDBCache.deleteDatabaseAndWait(dbName).catch(() => {});

  // Create version 1
  let cache = await IndexedDBCache.init({ dbName, version: 1 });

  Assert.notEqual(cache, null);
  Assert.equal(cache.db.version, 1);

  const model = "mozilla/distilvit";
  const revision = "main";
  const taskName = "echo";

  const blob = createBlob();

  await cache.put({
    taskName,
    model,
    revision,
    file: "file.txt",
    data: blob,
    headers: null,
  });

  cache.db.close();

  // Create version 2
  cache = await IndexedDBCache.init({ dbName, version: 2 });

  Assert.notEqual(cache, null);
  Assert.equal(cache.db.version, 2);

  // Ensure tables are all empty.
  const { files } = await cache.listFiles({ taskName });

  Assert.deepEqual(files, []);

  await deleteCache(cache);
});

/**
 * Test that upgrading an existing cache from another source is possible.
 */

add_task(async function test_initDbFromExistingElseWhereStoreChanges() {
  const dbName = `modelFiles-${crypto.randomUUID()}`;
  await TestIndexedDBCache.deleteDatabaseAndWait(dbName).catch(() => {});

  const dbVersion = 2;
  const model = "mozilla/distilvit";
  const revision = "main";
  const taskName = "echo";

  const blob = createBlob();
  // Create version 2
  const cache1 = await IndexedDBCache.init({ dbName, version: dbVersion });

  Assert.notEqual(cache1, null);
  Assert.equal(cache1.db.version, 2);

  // Cache1 is not closed by design of this test

  // Create version 3
  const cache2 = await IndexedDBCache.init({ dbName, version: dbVersion + 1 });

  Assert.notEqual(cache2, null);
  Assert.equal(cache2.db.version, 3);

  const when = await cache2.put({
    taskName,
    model,
    revision,
    file: "file.txt",
    data: blob,
    headers: null,
  });

  const expected = [
    {
      path: "file.txt",
      headers: {
        "Content-Type""application/octet-stream",
        fileSize: 8,
        ETag: "NO_ETAG",
        lastUpdated: when,
        lastUsed: when,
      },
      engineIds: [],
    },
  ];

  // Ensure every table & indices is on so that we can list files
  const { files } = await cache2.listFiles({ taskName, model, revision });
  Assert.deepEqual(files, expected);

  await deleteCache(cache2);
});

/**
 * Test that we can use a custom hub on every API call to get files.
 */

add_task(async function test_getting_file_custom_hub() {
  // The hub is configured to use localhost
  const hub = new ModelHub({
    rootUrl: "https://localhost",
    urlTemplate: "{model}/boo/revision",
  });

  // but we can use APIs against another hub
  const args = {
    model: "acme/bert",
    revision: "main",
    file: "config.json",
    taskName: "task_model",
    modelHubRootUrl: FAKE_HUB,
    modelHubUrlTemplate: "{model}/resolve/{revision}",
  };

  let [array, headers] = await hub.getModelFileAsArrayBuffer(args);

  Assert.equal(headers["Content-Type"], "application/json");

  // check the content of the file.
  let jsonData = JSON.parse(
    String.fromCharCode.apply(nullnew Uint8Array(array))
  );

  Assert.equal(jsonData.hidden_size, 32);

  let res = await hub.getModelFileAsBlob(args);
  Assert.equal(res[0].size, 548);

  let response = await hub.getModelFileAsResponse(args);
  Assert.equal((await response.blob()).size, 548);
});

/**
 * Make sure that we can't pass a rootUrl that is not allowed when using the API calls
 */

add_task(async function test_getting_file_disallowed_custom_hub() {
  // The hub is configured to use localhost
  const hub = new ModelHub({
    rootUrl: "https://localhost",
    urlTemplate: "{model}/boo/revision",
    allowDenyList: [{ filter: "ALLOW", urlPrefix: "https://example.com" }],
  });

  // and we can't use APIs against another hub if it's not allowed
  const args = {
    model: "acme/bert",
    revision: "main",
    file: "config.json",
    taskName: "task_model",
    modelHubRootUrl: "https://forbidden.com",
    modelHubUrlTemplate: "{model}/{revision}",
  };

  // This catch the error returned by getEtag when checking if file is in cache
  try {
    await hub.getModelFileAsArrayBuffer(args);
    throw new Error("Expected method to reject.");
  } catch (error) {
    Assert.throws(
      () => {
        throw error;
      },
      new RegExp(`ForbiddenURLError`),
      `Should throw with https://forbidden.com`
    );
  }

  // This catch the error returned when useCached is false
  try {
    await hub.getModelFileAsArrayBuffer({ ...args, revision: "v1" });
    throw new Error("Expected method to reject.");
  } catch (error) {
    Assert.throws(
      () => {
        throw error;
      },
      new RegExp(`ForbiddenURLError`),
      `Should throw with https://forbidden.com`
    );
  }

  try {
    await hub.getModelFileAsBlob(args);
    throw new Error("Expected method to reject.");
  } catch (error) {
    Assert.throws(
      () => {
        throw error;
      },
      new RegExp(`ForbiddenURLError`),
      `Should throw with https://forbidden.com`
    );
  }

  try {
    await hub.getModelFileAsResponse(args);
    throw new Error("Expected method to reject.");
  } catch (error) {
    Assert.throws(
      () => {
        throw error;
      },
      new RegExp(`ForbiddenURLError`),
      `Should throw with https://forbidden.com`
    );
  }

  // This catch the error when http error codes are returned, useCached is false
  try {
    await hub.getModelFileAsArrayBuffer({
      ...args,
      revision: "v1",
      modelHubRootUrl: "https://example.com",
    });
    throw new Error("Expected method to reject.");
  } catch (error) {
    Assert.throws(
      () => {
        throw error;
      },
      new RegExp(`HTTP error! Status: 404 Not Found`),
      `Should throw with 404`
    );
  }

  // This catch the error returned when useCached is true with no checks for etags
  try {
    // store a file in the hub
    await hub.cache.put({
      ...args,
      model: "forbidden.com/acme/bert",
      engineId: "engineOne",
      revision: "v1",
      data: createBlob(),
      headers: null,
    });

    await hub.getModelFileAsArrayBuffer({ ...args, revision: "v1" });
    throw new Error("Expected method to reject.");
  } catch (error) {
    Assert.throws(
      () => {
        throw error;
      },
      new RegExp(`ForbiddenURLError`),
      `Should throw with https://forbidden.com`
    );
  }
});

/**
 * Test deleting files used by several engines
 */

add_task(async function test_DeleteFileByEngines() {
  const cache = await initializeCache();
  const testData = createBlob();
  const engineOne = "engine-1";
  const engineTwo = "engine-2";
  const model = "org/model";
  const revision = "v1";

  // a file is stored by engineOne
  await cache.put({
    engineId: engineOne,
    taskName: "task",
    model: "org/model",
    revision: "v1",
    file: "file.txt",
    data: createBlob(),
    headers: null,
  });

  // The file is read by engineTwo
  let retrievedData = await cache.getFile({
    engineId: engineTwo,
    model: "org/model",
    revision: "v1",
    file: "file.txt",
  });

  Assert.deepEqual(
    retrievedData[0],
    testData,
    "The retrieved data should match the stored data."
  );

  // We should have two engines associated with the model file
  let retrievedFiles = await cache.listFiles({ model, revision });
  Assert.equal(retrievedFiles.metadata.engineIds.length, 2);

  // if we delete the model by engineOne, it will still be around for engineTwo
  await cache.deleteFilesByEngine({ engineId: engineOne });

  retrievedData = await cache.getFile({
    engineId: engineTwo,
    model: "org/model",
    revision: "v1",
    file: "file.txt",
  });
  Assert.deepEqual(
    retrievedData[0],
    testData,
    "The retrieved data should match the stored data."
  );

  // We should now have one engine associated with the model file
  retrievedFiles = await cache.listFiles({ model, revision });
  Assert.equal(retrievedFiles.metadata.engineIds.length, 1);
  Assert.equal(retrievedFiles.metadata.engineIds[0], engineTwo);

  // now deleting via engineTwo
  await cache.deleteFilesByEngine({ engineId: engineTwo });

  // at this point we should not have anymore files
  const dataAfterDelete = await cache.getFile({
    engineId: engineOne,
    model: "org/model",
    revision: "v1",
    file: "file.txt",
  });
  Assert.equal(
    dataAfterDelete,
    null,
    "The data for the deleted model should not exist."
  );

  // Now we should have no more engine
  Assert.equal(await cache._testGetData(cache.enginesStoreName), null);

  await deleteCache(cache);
});

/**
 * Test deleting files used by an engine via the model hub. This is similar to
 * `test_DeleteFileByEngines`, except we are calling the model hub method.
 */

add_task(async function test_ModelHub_DeleteFileByEngines() {
  const cache = await initializeCache();
  const engineOne = "engine-1";
  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
    allowDenyList: [],
  });
  hub.cache = cache;

  // a file is stored by engineOne
  await cache.put({
    engineId: engineOne,
    taskName: "task",
    model: "org/model",
    revision: "v1",
    file: "file.txt",
    data: createBlob(),
    headers: null,
  });

  // We should have at least one file
  const dataBeforeDelete = await cache.getFile({
    engineId: engineOne,
    model: "org/model",
    revision: "v1",
    file: "file.txt",
  });
  Assert.notEqual(dataBeforeDelete, null"The data should exist");

  // We should have at least one engine
  Assert.notEqual(await cache._testGetData(cache.enginesStoreName), null);

  await hub.deleteFilesByEngine({ engineId: engineOne });

  // at this point we should not have anymore files
  const dataAfterDelete = await cache.getFile({
    engineId: engineOne,
    model: "org/model",
    revision: "v1",
    file: "file.txt",
  });
  Assert.equal(
    dataAfterDelete,
    null,
    "The data for the deleted model should not exist."
  );

  // The engine should be removed from the model/revision/file engine list.
  // In our case, this means we should have no entry in the engine list since it is
  // the last
  Assert.equal(await cache._testGetData(cache.enginesStoreName), null);

  await deleteCache(cache);
});

// tests allow deny list updating after model is cached
add_task(async function test_update_allow_deny_after_model_cache() {
  const cache = await initializeCache();
  const file = "config.json";
  const taskName = FAKE_MODEL_ARGS.taskName;
  const model = FAKE_MODEL_ARGS.model;
  const revision = "v0.1";
  await cache.put({
    taskName,
    model,
    revision,
    file,
    data: createBlob(),
    headers: null,
  });

  let exists = await cache.fileExists({
    model,
    revision,
    file,
  });
  Assert.ok(exists, "The file should exist in the cache.");

  let list = [
    {
      filter: "ALLOW",
      urlPrefix:
        "chrome://mochitests/content/browser/toolkit/components/ml/tests/browser/data/acme",
    },
  ];

  let hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
    allowDenyList: list,
  });
  hub.cache = cache;
  // should go through since model is allowed
  await hub.getModelFileAsArrayBuffer({ ...FAKE_MODEL_ARGS, file, revision });

  // put model in deny list
  list = [
    {
      filter: "DENY",
      urlPrefix:
        "chrome://mochitests/content/browser/toolkit/components/ml/tests/browser/data/acme",
    },
  ];

  hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
    allowDenyList: list,
  });
  hub.cache = cache;

  // now ensure the model cannot be called after being put in the deny list
  try {
    await hub.getModelFileAsArrayBuffer({ ...FAKE_MODEL_ARGS, file, revision });
  } catch (e) {
    Assert.strictEqual(e.name, "ForbiddenURLError");
  }
  // make sure that the model is deleted after
  const dataAfterForbidden = await cache.getFile({
    model,
    revision,
    file,
  });
  Assert.equal(
    dataAfterForbidden,
    null,
    "The data for the deleted model should not exist."
  );
});

/**
 * Test that data from OPFS is wiped
 */

add_task(async function test_migrateStore_modelsDeleted() {
  const dbName = `modelFiles-${crypto.randomUUID()}`;
  await TestIndexedDBCache.deleteDatabaseAndWait(dbName).catch(() => {});

  // Initialize version 4 of the database
  let cache = await IndexedDBCache.init({ dbName, version: 4 });

  // Add some test data for unknown models
  await Promise.all([
    cache.put({
      taskName: "task",
      model: "random/model",
      revision: "v1",
      file: "random.txt",
      data: createBlob(),
      headers: null,
    }),
    cache.put({
      taskName: "task",
      model: "unknown/model",
      revision: "v2",
      file: "unknown.txt",
      data: createBlob(),
      headers: null,
    }),
  ]);

  // Close version 4 and upgrade to version 5
  cache.db.close();
  cache = await IndexedDBCache.init({ dbName, version: 5 });

  // Verify all unknown model data is deleted
  const { files: random } = await cache.listFiles({
    model: "random/model",
    revision: "v1",
  });
  Assert.deepEqual(random, [], "All unknown model files should be deleted.");

  const { files: unknown } = await cache.listFiles({
    model: "unknown/model",
    revision: "v2",
  });
  Assert.deepEqual(unknown, [], "All unknown model files should be deleted.");

  await deleteCache(cache);
});

/**
 * Test migration when database starts empty.
 */

add_task(async function test_migrateStore_emptyDatabase() {
  const dbName = `modelFiles-${crypto.randomUUID()}`;
  await TestIndexedDBCache.deleteDatabaseAndWait(dbName).catch(() => {});

  // Initialize an empty version 4 database
  let cache = await IndexedDBCache.init({ dbName, version: 4 });
  cache.db.close();

  // Upgrade to version 5
  cache = await IndexedDBCache.init({ dbName, version: 5 });

  // Verify database is still empty
  const models = await cache.listModels();
  Assert.deepEqual(
    models,
    [],
    "The database should remain empty after migration."
  );

  await deleteCache(cache);
});

add_task(async function test_getOwnerIcon_cache() {
  await SpecialPowers.pushPrefEnv({
    set: [["browser.ml.logLevel""All"]],
  });

  const originalOPFSFile = OPFS.File;

  const localPaths = new Set();
  const stub = sinon.stub(OPFS, "File").callsFake(function (args) {
    localPaths.add(args.localPath);
    return new originalOPFSFile(args); // preserve original behavior
  });

  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
  });

  const fullyQualifiedModelName = `mochitests/mozilla/distilvit-${crypto.randomUUID()}`;

  // first call will get the icon from the web
  const icon = await hub.getOwnerIcon(fullyQualifiedModelName);
  Assert.notEqual(icon, null);

  // second call will get it from the cache
  let spy = sinon.spy(Progress, "fetchUrl");

  const icon2 = await hub.getOwnerIcon(fullyQualifiedModelName);
  Assert.notEqual(icon2, null);

  // check that it cames from OPFS
  Assert.equal(spy.called, false);

  spy.restore();
  stub.restore();
  for (const path of localPaths) {
    await OPFS.remove(path, { recursive: true });
  }
});

add_task(async function test_getOwnerIcon_download() {
  await SpecialPowers.pushPrefEnv({
    set: [["browser.ml.logLevel""All"]],
  });

  const originalOPFSFile = OPFS.File;

  const localPaths = new Set();
  const stub = sinon.stub(OPFS, "File").callsFake(function (args) {
    localPaths.add(args.localPath);
    return new originalOPFSFile(args); // preserve original behavior
  });

  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
  });

  const fullyQualifiedModelName = `mochitests/mozilla/distilvit-${crypto.randomUUID()}`;

  let spy = sinon.spy(Progress, "fetchUrl");
  // first call will get the icon from the web
  const icon = await hub.getOwnerIcon(fullyQualifiedModelName);
  Assert.notEqual(icon, null);
  // check that it didn't come from OPFS
  Assert.equal(spy.called, true);
  Assert.notEqual(await spy.lastCall?.returnValue, null);

  spy.restore();
  stub.restore();
  for (const path of localPaths) {
    await OPFS.remove(path, { recursive: true });
  }
});

/**
 * Test that downloads can be cancelled
 */

add_task(async function test_download_cancellation() {
  const cache = await initializeCache();
  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
  });
  hub.cache = cache;

  const controller = new AbortController();
  const { signal } = controller;

  try {
    controller.abort();

    await Assert.rejects(
      hub.getModelDataAsFile({ ...FAKE_MODEL_ARGS, abortSignal: signal }),
      err => err?.name === "AbortError",
      "The call should be cancelled"
    );
  } catch (err) {
    Assert.ok(false, `Expected AbortError. Got ${err}`);
  } finally {
    await deleteCache(cache);
  }
});

/**
 * Test that downloads can be cancelled after fetch is successfull
 */

add_task(async function test_download_cancellation_after_fetch() {
  const cache = await initializeCache();
  const hub = new ModelHub({
    rootUrl: FAKE_HUB,
    urlTemplate: FAKE_URL_TEMPLATE,
  });
  hub.cache = cache;

  const controller = new AbortController();
  const { signal } = controller;

  const fetchUrlStub = sinon
    .stub(MLUtils, "fetchUrl")
    .callsFake((url, { signal: _, ...rest } = {}) => {
      const p = fetch(url, rest);
      controller.abort();
      return p;
    });

  try {
    await Assert.rejects(
      hub.getModelDataAsFile({ ...FAKE_MODEL_ARGS, abortSignal: signal }),
      err => err?.name === "AbortError",
      "The call should be cancelled"
    );
  } catch (err) {
    Assert.ok(false, `Expected AbortError. Got ${err}`);
  } finally {
    await deleteCache(cache);
    fetchUrlStub.restore();
  }
});

/**
 * Bug 1967279: Verify the 6 -> 7 schema migration rewrites a row tagged with
 * the old "wllamapreview" engineId to "link-preview" without dropping the
 * underlying OPFS model file, so the renamed feature can still uninstall it.
 */

add_task(async function test_migrateStore_renamesWllamapreviewEngineId() {
  const dbName = `modelFiles-${crypto.randomUUID()}`;
  await TestIndexedDBCache.deleteDatabaseAndWait(dbName).catch(() => {});

  const model = "org/link-preview";
  const revision = "v1";
  const file = "weights.bin";

  // Seed a row under the pre-migration schema with the obsolete engineId.
  let cache = await IndexedDBCache.init({ dbName, version: 6 });
  await cache.put({
    engineId: "wllamapreview",
    taskName: "wllama-text-generation",
    model,
    revision,
    file,
    data: createBlob(),
    headers: null,
  });

  let listed = await cache.listFiles({ model, revision });
  Assert.deepEqual(
    listed.metadata.engineIds,
    ["wllamapreview"],
    "Row should be seeded with the legacy engineId."
  );
  cache.db.close();

  // Reopen at the current schema version and confirm the rename happened.
  cache = await IndexedDBCache.init({ dbName });

  listed = await cache.listFiles({ model, revision });
  Assert.deepEqual(
    listed.metadata.engineIds,
    ["link-preview"],
    "Migration should rewrite the legacy engineId to its replacement."
  );
  Assert.equal(
    listed.files.length,
    1,
    "Migration must not drop the OPFS model file."
  );

  // The uninstall path used by LinkPreview should now find and remove the row.
  await cache.deleteFilesByEngine({ engineId: "link-preview" });
  listed = await cache.listFiles({ model, revision });
  Assert.deepEqual(
    listed.files,
    [],
    "Uninstall via the new engineId should physically delete the file."
  );

  await deleteCache(cache);
});

/**
 * Bug 1967279: If a row already carries both the legacy and the new engineId
 * (e.g. a re-download happened before the migration), the rewrite must
 * de-duplicate rather than leaving the stale id behind.
 */

add_task(async function test_migrateStore_dedupsLegacyAndNewEngineIds() {
  const dbName = `modelFiles-${crypto.randomUUID()}`;
  await TestIndexedDBCache.deleteDatabaseAndWait(dbName).catch(() => {});

  const model = "org/link-preview";
  const revision = "v1";
  const file = "weights.bin";

  let cache = await IndexedDBCache.init({ dbName, version: 6 });
  await cache.put({
    engineId: "wllamapreview",
    taskName: "wllama-text-generation",
    model,
    revision,
    file,
    data: createBlob(),
    headers: null,
  });
  await cache.put({
    engineId: "link-preview",
    taskName: "wllama-text-generation",
    model,
    revision,
    file,
    data: createBlob(),
    headers: null,
  });
  cache.db.close();

  cache = await IndexedDBCache.init({ dbName });
  const listed = await cache.listFiles({ model, revision });
  Assert.deepEqual(
    listed.metadata.engineIds,
    ["link-preview"],
    "Migration should collapse duplicate ids to the new engineId only."
  );

  await deleteCache(cache);
});

Messung V0.5 in Prozent
C=97 H=97 G=96

¤ Dauer der Verarbeitung: 0.26 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover

NIST Cobol Testsuite

Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Statistik
#Sources=277311
#Domains=752002