From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- comm/calendar/test/unit/providers/head.js | 152 ++++++++++++++++ .../unit/providers/test_caldavCalendar_cached.js | 201 +++++++++++++++++++++ .../unit/providers/test_caldavCalendar_uncached.js | 96 ++++++++++ .../test/unit/providers/test_icsCalendar_cached.js | 53 ++++++ .../unit/providers/test_icsCalendar_uncached.js | 46 +++++ .../test/unit/providers/test_storageCalendar.js | 17 ++ comm/calendar/test/unit/providers/xpcshell.ini | 11 ++ 7 files changed, 576 insertions(+) create mode 100644 comm/calendar/test/unit/providers/head.js create mode 100644 comm/calendar/test/unit/providers/test_caldavCalendar_cached.js create mode 100644 comm/calendar/test/unit/providers/test_caldavCalendar_uncached.js create mode 100644 comm/calendar/test/unit/providers/test_icsCalendar_cached.js create mode 100644 comm/calendar/test/unit/providers/test_icsCalendar_uncached.js create mode 100644 comm/calendar/test/unit/providers/test_storageCalendar.js create mode 100644 comm/calendar/test/unit/providers/xpcshell.ini (limited to 'comm/calendar/test/unit/providers') diff --git a/comm/calendar/test/unit/providers/head.js b/comm/calendar/test/unit/providers/head.js new file mode 100644 index 0000000000..3c995ab31a --- /dev/null +++ b/comm/calendar/test/unit/providers/head.js @@ -0,0 +1,152 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); +var { CalendarTestUtils } = ChromeUtils.import( + "resource://testing-common/calendar/CalendarTestUtils.jsm" +); +var { CalEvent } = ChromeUtils.import("resource:///modules/CalEvent.jsm"); +var { PromiseUtils } = ChromeUtils.importESModule("resource://gre/modules/PromiseUtils.sys.mjs"); + +var { updateAppInfo } = ChromeUtils.importESModule("resource://testing-common/AppInfo.sys.mjs"); +updateAppInfo(); + +// The tests in this directory each do the same thing, with slight variations as needed for each +// calendar provider. The core of the test lives in this file and the tests call it when ready. + +do_get_profile(); +add_setup(async () => { + await new Promise(resolve => cal.manager.startup({ onResult: resolve })); + await new Promise(resolve => cal.timezoneService.startup({ onResult: resolve })); + cal.manager.addCalendarObserver(calendarObserver); +}); + +let calendarObserver = { + QueryInterface: ChromeUtils.generateQI(["calIObserver"]), + + /* calIObserver */ + + _batchCount: 0, + _batchRequired: true, + onStartBatch(calendar) { + info(`onStartBatch ${calendar?.id} ${++this._batchCount}`); + Assert.equal(calendar, this._expectedCalendar); + Assert.equal(this._batchCount, 1, "onStartBatch must not occur in a batch"); + }, + onEndBatch(calendar) { + info(`onEndBatch ${calendar?.id} ${this._batchCount--}`); + Assert.equal(calendar, this._expectedCalendar); + Assert.equal(this._batchCount, 0, "onEndBatch must occur in a batch"); + }, + onLoad(calendar) { + info(`onLoad ${calendar.id}`); + Assert.equal(this._batchCount, 0, "onLoad must not occur in a batch"); + Assert.equal(calendar, this._expectedCalendar); + if (this._onLoadPromise) { + this._onLoadPromise.resolve(); + } + }, + onAddItem(item) { + info(`onAddItem ${item.calendar.id} ${item.id}`); + if (this._batchRequired) { + Assert.equal(this._batchCount, 1, "onAddItem must occur in a batch"); + } + if (this._onAddItemPromise) { + this._onAddItemPromise.resolve(); + } + }, + onModifyItem(newItem, oldItem) { + info(`onModifyItem ${newItem.calendar.id} ${newItem.id}`); + if (this._batchRequired) { + Assert.equal(this._batchCount, 1, "onModifyItem must occur in a batch"); + } + if (this._onModifyItemPromise) { + this._onModifyItemPromise.resolve(); + } + }, + onDeleteItem(deletedItem) { + info(`onDeleteItem ${deletedItem.calendar.id} ${deletedItem.id}`); + if (this._onDeleteItemPromise) { + this._onDeleteItemPromise.resolve(); + } + }, + onError(calendar, errNo, message) {}, + onPropertyChanged(calendar, name, value, oldValue) {}, + onPropertyDeleting(calendar, name) {}, +}; + +/** + * Create and register a calendar. + * + * @param {string} type - The calendar provider to use. + * @param {string} url - URL of the server. + * @param {boolean} useCache - Should this calendar have offline storage? + * @returns {calICalendar} + */ +function createCalendar(type, url, useCache) { + let calendar = cal.manager.createCalendar(type, Services.io.newURI(url)); + calendar.name = type + (useCache ? " with cache" : " without cache"); + calendar.id = cal.getUUID(); + calendar.setProperty("cache.enabled", useCache); + + cal.manager.registerCalendar(calendar); + calendar = cal.manager.getCalendarById(calendar.id); + calendarObserver._expectedCalendar = calendar; + + info(`Created calendar ${calendar.id}`); + return calendar; +} + +/** + * Creates an event and adds it to the given calendar. + * + * @param {calICalendar} calendar + * @returns {calIEvent} + */ +async function runAddItem(calendar) { + let event = new CalEvent(); + event.id = "6b7dd6f6-d6f0-4e93-a953-bb5473c4c47a"; + event.title = "New event"; + event.startDate = cal.createDateTime("20200303T205500Z"); + event.endDate = cal.createDateTime("20200303T210200Z"); + + calendarObserver._onAddItemPromise = PromiseUtils.defer(); + calendarObserver._onModifyItemPromise = PromiseUtils.defer(); + await calendar.addItem(event); + await Promise.any([ + calendarObserver._onAddItemPromise.promise, + calendarObserver._onModifyItemPromise.promise, + ]); + + return event; +} + +/** + * Modifies the event from runAddItem. + * + * @param {calICalendar} calendar + */ +async function runModifyItem(calendar) { + let event = await calendar.getItem("6b7dd6f6-d6f0-4e93-a953-bb5473c4c47a"); + + let clone = event.clone(); + clone.title = "Modified event"; + + calendarObserver._onModifyItemPromise = PromiseUtils.defer(); + await calendar.modifyItem(clone, event); + await calendarObserver._onModifyItemPromise.promise; +} + +/** + * Deletes the event from runAddItem. + * + * @param {calICalendar} calendar + */ +async function runDeleteItem(calendar) { + let event = await calendar.getItem("6b7dd6f6-d6f0-4e93-a953-bb5473c4c47a"); + + calendarObserver._onDeleteItemPromise = PromiseUtils.defer(); + await calendar.deleteItem(event); + await calendarObserver._onDeleteItemPromise.promise; +} diff --git a/comm/calendar/test/unit/providers/test_caldavCalendar_cached.js b/comm/calendar/test/unit/providers/test_caldavCalendar_cached.js new file mode 100644 index 0000000000..7a61973644 --- /dev/null +++ b/comm/calendar/test/unit/providers/test_caldavCalendar_cached.js @@ -0,0 +1,201 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { CalDAVServer } = ChromeUtils.import("resource://testing-common/calendar/CalDAVServer.jsm"); + +add_setup(async function () { + CalDAVServer.open(); + await CalDAVServer.putItemInternal( + "5a9fa76c-93f3-4ad8-9f00-9e52aedd2821.ics", + CalendarTestUtils.dedent` + BEGIN:VCALENDAR + BEGIN:VEVENT + UID:5a9fa76c-93f3-4ad8-9f00-9e52aedd2821 + SUMMARY:exists before time + DTSTART:20210401T120000Z + DTEND:20210401T130000Z + END:VEVENT + END:VCALENDAR + ` + ); +}); +registerCleanupFunction(() => CalDAVServer.close()); + +add_task(async function () { + calendarObserver._onAddItemPromise = PromiseUtils.defer(); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + let calendar = createCalendar("caldav", CalDAVServer.url, true); + await calendarObserver._onAddItemPromise.promise; + await calendarObserver._onLoadPromise.promise; + info("calendar set-up complete"); + + Assert.ok(await calendar.getItem("5a9fa76c-93f3-4ad8-9f00-9e52aedd2821")); + + info("creating the item"); + calendarObserver._batchRequired = true; + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runAddItem(calendar); + await calendarObserver._onLoadPromise.promise; + + info("modifying the item"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runModifyItem(calendar); + await calendarObserver._onLoadPromise.promise; + + info("deleting the item"); + await runDeleteItem(calendar); + + cal.manager.unregisterCalendar(calendar); +}); + +/** + * Tests calendars that return status 404 for "current-user-privilege-set" are + * not flagged read-only. + */ +add_task(async function testCalendarWithNoPrivSupport() { + CalDAVServer.privileges = null; + calendarObserver._onLoadPromise = PromiseUtils.defer(); + + let calendar = createCalendar("caldav", CalDAVServer.url, true); + await calendarObserver._onLoadPromise.promise; + info("calendar set-up complete"); + + Assert.ok(!calendar.readOnly, "calendar was not marked read-only"); + + cal.manager.unregisterCalendar(calendar); +}); + +/** + * Tests modifyItem() does not hang when the server reports no actual + * modifications were made. + */ +add_task(async function testModifyItemWithNoChanges() { + let event = new CalEvent(); + let calendar = createCalendar("caldav", CalDAVServer.url, false); + event.id = "6f6dd7b6-0fbd-39e4-359a-a74c4c3745bb"; + event.title = "A New Event"; + event.startDate = cal.createDateTime("20200303T205500Z"); + event.endDate = cal.createDateTime("20200303T210200Z"); + await calendar.addItem(event); + + let clone = event.clone(); + clone.title = "A Modified Event"; + + let putItemInternal = CalDAVServer.putItemInternal; + CalDAVServer.putItemInternal = () => {}; + + let modifiedEvent = await calendar.modifyItem(clone, event); + CalDAVServer.putItemInternal = putItemInternal; + + Assert.ok(modifiedEvent, "an event was returned"); + Assert.equal(modifiedEvent.title, event.title, "the un-modified event is returned"); + + await calendar.deleteItem(modifiedEvent); + cal.manager.unregisterCalendar(calendar); +}); + +/** + * Tests that an error response from the server when syncing doesn't delete + * items from the local calendar. + */ +add_task(async function testSyncError1() { + calendarObserver._onAddItemPromise = PromiseUtils.defer(); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + let calendar = createCalendar("caldav", CalDAVServer.url, true); + await calendarObserver._onAddItemPromise.promise; + await calendarObserver._onLoadPromise.promise; + info("calendar set-up complete"); + + Assert.ok( + await calendar.getItem("5a9fa76c-93f3-4ad8-9f00-9e52aedd2821"), + "item should exist when first connected" + ); + + info("syncing with rate limit error"); + CalDAVServer.throwRateLimitErrors = true; + calendarObserver._onLoadPromise = PromiseUtils.defer(); + calendar.refresh(); + await calendarObserver._onLoadPromise.promise; + CalDAVServer.throwRateLimitErrors = false; + info("sync with rate limit error complete"); + + Assert.equal( + calendar.getProperty("currentStatus"), + Cr.NS_OK, + "calendar should not be in an error state" + ); + Assert.equal(calendar.getProperty("disabled"), null, "calendar should not be disabled"); + Assert.ok( + await calendar.getItem("5a9fa76c-93f3-4ad8-9f00-9e52aedd2821"), + "item should still exist after error response" + ); + + info("syncing without rate limit error"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + calendar.refresh(); + await calendarObserver._onLoadPromise.promise; + info("sync without rate limit error complete"); + + Assert.ok( + await calendar.getItem("5a9fa76c-93f3-4ad8-9f00-9e52aedd2821"), + "item should still exist after successful sync" + ); + + cal.manager.unregisterCalendar(calendar); +}); + +/** + * Tests that multiple pages of item responses from the server when syncing + * doesn't result in items being deleted from the local calendar. + * + * The server has a page size of 3, although this test should pass regardless + * of the page size. + */ +add_task(async function testSyncError2() { + // Add some items to the server so multiple requests are required to get + // them all. There's already one item on the server. + for (let i = 0; i < 3; i++) { + await CalDAVServer.putItemInternal( + `fake-uid-${i}.ics`, + CalendarTestUtils.dedent` + BEGIN:VCALENDAR + BEGIN:VEVENT + UID:fake-uid-${i} + SUMMARY:event ${i} + DTSTART:20210401T120000Z + DTEND:20210401T130000Z + END:VEVENT + END:VCALENDAR + ` + ); + } + + calendarObserver._onAddItemPromise = PromiseUtils.defer(); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + let calendar = createCalendar("caldav", CalDAVServer.url, true); + await calendarObserver._onAddItemPromise.promise; + await calendarObserver._onLoadPromise.promise; + info("calendar set-up complete"); + + let items = await calendar.getItemsAsArray(Ci.calICalendar.ITEM_FILTER_TYPE_ALL, 0, null, null); + Assert.equal(items.length, 4, "all items added to calendar when first connected"); + + info("forced syncing with multiple pages"); + calendar.wrappedJSObject.mUncachedCalendar.wrappedJSObject.mWebdavSyncToken = null; + calendar.wrappedJSObject.mUncachedCalendar.wrappedJSObject.saveCalendarProperties(); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + calendar.refresh(); + await calendarObserver._onLoadPromise.promise; + info("forced sync with multiple pages complete"); + + items = await calendar.getItemsAsArray(Ci.calICalendar.ITEM_FILTER_TYPE_ALL, 0, null, null); + Assert.equal(items.length, 4, "all items still in calendar after forced refresh"); + + cal.manager.unregisterCalendar(calendar); + + // Delete the added items. + for (let i = 0; i < 3; i++) { + CalDAVServer.deleteItemInternal(`fake-uid-${i}.ics`); + } +}); diff --git a/comm/calendar/test/unit/providers/test_caldavCalendar_uncached.js b/comm/calendar/test/unit/providers/test_caldavCalendar_uncached.js new file mode 100644 index 0000000000..025a1ba871 --- /dev/null +++ b/comm/calendar/test/unit/providers/test_caldavCalendar_uncached.js @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { CalDAVServer } = ChromeUtils.import("resource://testing-common/calendar/CalDAVServer.jsm"); + +add_setup(async function () { + CalDAVServer.open(); + await CalDAVServer.putItemInternal( + "5a9fa76c-93f3-4ad8-9f00-9e52aedd2821.ics", + CalendarTestUtils.dedent` + BEGIN:VCALENDAR + BEGIN:VEVENT + UID:5a9fa76c-93f3-4ad8-9f00-9e52aedd2821 + SUMMARY:exists before time + DTSTART:20210401T120000Z + DTEND:20210401T130000Z + END:VEVENT + END:VCALENDAR + ` + ); +}); +registerCleanupFunction(() => CalDAVServer.close()); + +add_task(async function () { + calendarObserver._onAddItemPromise = PromiseUtils.defer(); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + let calendar = createCalendar("caldav", CalDAVServer.url, false); + await calendarObserver._onAddItemPromise.promise; + await calendarObserver._onLoadPromise.promise; + info("calendar set-up complete"); + + Assert.ok(await calendar.getItem("5a9fa76c-93f3-4ad8-9f00-9e52aedd2821")); + + info("creating the item"); + calendarObserver._batchRequired = true; + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runAddItem(calendar); + await calendarObserver._onLoadPromise.promise; + + info("modifying the item"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runModifyItem(calendar); + await calendarObserver._onLoadPromise.promise; + + info("deleting the item"); + await runDeleteItem(calendar); + + cal.manager.unregisterCalendar(calendar); +}); + +/** + * Tests calendars that return status 404 for "current-user-privilege-set" are + * not flagged read-only. + */ +add_task(async function testCalendarWithNoPrivSupport() { + CalDAVServer.privileges = null; + calendarObserver._onLoadPromise = PromiseUtils.defer(); + + let calendar = createCalendar("caldav", CalDAVServer.url, false); + await calendarObserver._onLoadPromise.promise; + info("calendar set-up complete"); + + Assert.ok(!calendar.readOnly, "calendar was not marked read-only"); + + cal.manager.unregisterCalendar(calendar); +}); + +/** + * Tests modifyItem() does not hang when the server reports no actual + * modifications were made. + */ +add_task(async function testModifyItemWithNoChanges() { + let event = new CalEvent(); + let calendar = createCalendar("caldav", CalDAVServer.url, false); + event.id = "6f6dd7b6-0fbd-39e4-359a-a74c4c3745bb"; + event.title = "A New Event"; + event.startDate = cal.createDateTime("20200303T205500Z"); + event.endDate = cal.createDateTime("20200303T210200Z"); + await calendar.addItem(event); + + let clone = event.clone(); + clone.title = "A Modified Event"; + + let putItemInternal = CalDAVServer.putItemInternal; + CalDAVServer.putItemInternal = () => {}; + + let modifiedEvent = await calendar.modifyItem(clone, event); + CalDAVServer.putItemInternal = putItemInternal; + + Assert.ok(modifiedEvent, "an event was returned"); + Assert.equal(modifiedEvent.title, event.title, "the un-modified event is returned"); + + await calendar.deleteItem(modifiedEvent); + cal.manager.unregisterCalendar(calendar); +}); diff --git a/comm/calendar/test/unit/providers/test_icsCalendar_cached.js b/comm/calendar/test/unit/providers/test_icsCalendar_cached.js new file mode 100644 index 0000000000..9bc1b127da --- /dev/null +++ b/comm/calendar/test/unit/providers/test_icsCalendar_cached.js @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { ICSServer } = ChromeUtils.import("resource://testing-common/calendar/ICSServer.jsm"); + +ICSServer.open(); +ICSServer.putICSInternal( + CalendarTestUtils.dedent` + BEGIN:VCALENDAR + BEGIN:VEVENT + UID:5a9fa76c-93f3-4ad8-9f00-9e52aedd2821 + SUMMARY:exists before time + DTSTART:20210401T120000Z + DTEND:20210401T130000Z + END:VEVENT + END:VCALENDAR + ` +); +registerCleanupFunction(() => ICSServer.close()); + +add_task(async function () { + // TODO: item notifications from a cached ICS calendar occur outside of batches. + // This isn't fatal but it shouldn't happen. Side-effects include alarms firing + // twice - once from onAddItem then again at onLoad. + // + // Remove the next line when this is fixed. + calendarObserver._batchRequired = false; + + calendarObserver._onAddItemPromise = PromiseUtils.defer(); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + let calendar = createCalendar("ics", ICSServer.url, true); + await calendarObserver._onAddItemPromise.promise; + await calendarObserver._onLoadPromise.promise; + info("calendar set-up complete"); + + Assert.ok(await calendar.getItem("5a9fa76c-93f3-4ad8-9f00-9e52aedd2821")); + + info("creating the item"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runAddItem(calendar); + await calendarObserver._onLoadPromise.promise; + + info("modifying the item"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runModifyItem(calendar); + await calendarObserver._onLoadPromise.promise; + + info("deleting the item"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runDeleteItem(calendar); + await calendarObserver._onLoadPromise.promise; +}); diff --git a/comm/calendar/test/unit/providers/test_icsCalendar_uncached.js b/comm/calendar/test/unit/providers/test_icsCalendar_uncached.js new file mode 100644 index 0000000000..db5db6e99f --- /dev/null +++ b/comm/calendar/test/unit/providers/test_icsCalendar_uncached.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { ICSServer } = ChromeUtils.import("resource://testing-common/calendar/ICSServer.jsm"); + +ICSServer.open(); +ICSServer.putICSInternal( + CalendarTestUtils.dedent` + BEGIN:VCALENDAR + BEGIN:VEVENT + UID:5a9fa76c-93f3-4ad8-9f00-9e52aedd2821 + SUMMARY:exists before time + DTSTART:20210401T120000Z + DTEND:20210401T130000Z + END:VEVENT + END:VCALENDAR + ` +); +registerCleanupFunction(() => ICSServer.close()); + +add_task(async function () { + calendarObserver._onAddItemPromise = PromiseUtils.defer(); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + let calendar = createCalendar("ics", ICSServer.url, false); + await calendarObserver._onAddItemPromise.promise; + await calendarObserver._onLoadPromise.promise; + info("calendar set-up complete"); + + Assert.ok(await calendar.getItem("5a9fa76c-93f3-4ad8-9f00-9e52aedd2821")); + + info("creating the item"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runAddItem(calendar); + await calendarObserver._onLoadPromise.promise; + + info("modifying the item"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runModifyItem(calendar); + await calendarObserver._onLoadPromise.promise; + + info("deleting the item"); + calendarObserver._onLoadPromise = PromiseUtils.defer(); + await runDeleteItem(calendar); + await calendarObserver._onLoadPromise.promise; +}); diff --git a/comm/calendar/test/unit/providers/test_storageCalendar.js b/comm/calendar/test/unit/providers/test_storageCalendar.js new file mode 100644 index 0000000000..778f9f9251 --- /dev/null +++ b/comm/calendar/test/unit/providers/test_storageCalendar.js @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +add_task(async function () { + let calendar = createCalendar("storage", "moz-storage-calendar://"); + + info("creating the item"); + calendarObserver._batchRequired = false; + await runAddItem(calendar); + + info("modifying the item"); + await runModifyItem(calendar); + + info("deleting the item"); + await runDeleteItem(calendar); +}); diff --git a/comm/calendar/test/unit/providers/xpcshell.ini b/comm/calendar/test/unit/providers/xpcshell.ini new file mode 100644 index 0000000000..272fef7c78 --- /dev/null +++ b/comm/calendar/test/unit/providers/xpcshell.ini @@ -0,0 +1,11 @@ +[default] +head = head.js +prefs = + calendar.timezone.local=UTC + calendar.timezone.useSystemTimezone=false + +[test_caldavCalendar_cached.js] +[test_caldavCalendar_uncached.js] +[test_icsCalendar_cached.js] +[test_icsCalendar_uncached.js] +[test_storageCalendar.js] -- cgit v1.2.3