/**************************************************************************************** * Copyright (c) 2016 Houssam Salem <houssam.salem.au@gmail.com> * * * * This program is free software; you can redistribute it and/or modify it under * * the terms of the GNU General Public License as published by the Free Software * * Foundation; either version 3 of the License, or (at your option) any later * * version. * * * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License along with * * this program. If not, see <http://www.gnu.org/licenses/>. * ****************************************************************************************/ package com.ichi2.anki.tests.libanki; import android.test.AndroidTestCase; import android.test.suitebuilder.annotation.Suppress; import com.ichi2.anki.exception.ConfirmModSchemaException; import com.ichi2.anki.tests.Shared; import com.ichi2.libanki.Collection; import com.ichi2.libanki.Models; import com.ichi2.libanki.Note; import com.ichi2.libanki.Utils; import com.ichi2.libanki.importer.Anki2Importer; import com.ichi2.libanki.importer.AnkiPackageImporter; import com.ichi2.libanki.importer.Importer; import com.ichi2.libanki.importer.TextImporter; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.List; public class ImportTest extends AndroidTestCase { public void testAnki2Mediadupes() throws IOException, JSONException { List<String> expected; List<String> actual; Collection tmp = Shared.getEmptyCol(getContext()); // add a note that references a sound Note n = tmp.newNote(); n.setItem("Front", "[sound:foo.mp3]"); long mid = n.model().getLong("id"); tmp.addNote(n); // add that sound to the media folder FileOutputStream os; os = new FileOutputStream(new File(tmp.getMedia().dir(), "foo.mp3"), false); os.write("foo".getBytes()); os.close(); tmp.close(); // it should be imported correctly into an empty deck Collection empty = Shared.getEmptyCol(getContext()); Importer imp = new Anki2Importer(empty, tmp.getPath()); imp.run(); expected = Arrays.asList("foo.mp3"); actual = Arrays.asList(new File(empty.getMedia().dir()).list()); actual.retainAll(expected); assertEquals(expected.size(), actual.size()); // and importing again will not duplicate, as the file content matches empty.remCards(Utils.arrayList2array(empty.getDb().queryColumn(Long.class, "select id from cards", 0))); imp = new Anki2Importer(empty, tmp.getPath()); imp.run(); expected = Arrays.asList("foo.mp3"); actual = Arrays.asList(new File(empty.getMedia().dir()).list()); actual.retainAll(expected); assertEquals(expected.size(), actual.size()); n = empty.getNote(empty.getDb().queryLongScalar("select id from notes")); assertTrue(n.getFields()[0].contains("foo.mp3")); // if the local file content is different, and import should trigger a rename empty.remCards(Utils.arrayList2array(empty.getDb().queryColumn(Long.class, "select id from cards", 0))); os = new FileOutputStream(new File(empty.getMedia().dir(), "foo.mp3"), false); os.write("bar".getBytes()); os.close(); imp = new Anki2Importer(empty, tmp.getPath()); imp.run(); expected = Arrays.asList("foo.mp3", String.format("foo_%s.mp3", mid)); actual = Arrays.asList(new File(empty.getMedia().dir()).list()); actual.retainAll(expected); assertEquals(expected.size(), actual.size()); n = empty.getNote(empty.getDb().queryLongScalar("select id from notes")); assertTrue(n.getFields()[0].contains("_")); // if the localized media file already exists, we rewrite the note and media empty.remCards(Utils.arrayList2array(empty.getDb().queryColumn(Long.class, "select id from cards", 0))); os = new FileOutputStream(new File(empty.getMedia().dir(), "foo.mp3")); os.write("bar".getBytes()); os.close(); imp = new Anki2Importer(empty, tmp.getPath()); imp.run(); expected = Arrays.asList("foo.mp3", String.format("foo_%s.mp3", mid)); actual = Arrays.asList(new File(empty.getMedia().dir()).list()); actual.retainAll(expected); assertEquals(expected.size(), actual.size()); n = empty.getNote(empty.getDb().queryLongScalar("select id from notes")); assertTrue(n.getFields()[0].contains("_")); } public void testApkg() throws IOException { List<String> expected; List<String> actual; Collection tmp = Shared.getEmptyCol(getContext()); String apkg = Shared.getTestFilePath(getContext(), "media.apkg"); Importer imp = new AnkiPackageImporter(tmp, apkg); expected = Arrays.asList(); actual = Arrays.asList(new File(tmp.getMedia().dir()).list()); actual.retainAll(expected); assertEquals(actual.size(), expected.size()); imp.run(); expected = Arrays.asList("foo.wav"); actual = Arrays.asList(new File(tmp.getMedia().dir()).list()); actual.retainAll(expected); assertEquals(expected.size(), actual.size()); // import again should be idempotent in terms of media tmp.remCards(Utils.arrayList2array(tmp.getDb().queryColumn(Long.class, "select id from cards", 0))); imp = new AnkiPackageImporter(tmp, apkg); imp.run(); expected = Arrays.asList("foo.wav"); actual = Arrays.asList(new File(tmp.getMedia().dir()).list()); actual.retainAll(expected); assertEquals(actual.size(), expected.size()); // but if the local file has different data, it will rename tmp.remCards(Utils.arrayList2array(tmp.getDb().queryColumn(Long.class, "select id from cards", 0))); FileOutputStream os; os = new FileOutputStream(new File(tmp.getMedia().dir(), "foo.wav"), false); os.write("xyz".getBytes()); os.close(); imp = new AnkiPackageImporter(tmp, apkg); imp.run(); assertTrue(new File(tmp.getMedia().dir()).list().length == 2); } public void testAnki2Diffmodels() throws IOException { // create a new empty deck Collection dst = Shared.getEmptyCol(getContext()); // import the 1 card version of the model String tmp = Shared.getTestFilePath(getContext(), "diffmodels2-1.apkg"); AnkiPackageImporter imp = new AnkiPackageImporter(dst, tmp); imp.setDupeOnSchemaChange(true); imp.run(); int before = dst.noteCount(); // repeating the process should do nothing imp = new AnkiPackageImporter(dst, tmp); imp.setDupeOnSchemaChange(true); imp.run(); assertTrue(before == dst.noteCount()); // then the 2 card version tmp = Shared.getTestFilePath(getContext(), "diffmodels2-2.apkg"); imp = new AnkiPackageImporter(dst, tmp); imp.setDupeOnSchemaChange(true); imp.run(); int after = dst.noteCount(); // as the model schemas differ, should have been imported as new model assertTrue(after == before + 1); // and the new model should have both cards assertTrue(dst.cardCount() == 3); // repeating the process should do nothing imp = new AnkiPackageImporter(dst, tmp); imp.setDupeOnSchemaChange(true); imp.run(); after = dst.noteCount(); assertTrue(after == before + 1); assertTrue(dst.cardCount() == 3); } public void testAnki2DiffmodelTemplates() throws IOException, JSONException { // different from the above as this one tests only the template text being // changed, not the number of cards/fields Collection dst = Shared.getEmptyCol(getContext()); // import the first version of the model String tmp = Shared.getTestFilePath(getContext(), "diffmodeltemplates-1.apkg"); AnkiPackageImporter imp = new AnkiPackageImporter(dst, tmp); imp.setDupeOnSchemaChange(true); imp.run(); // then the version with updated template tmp = Shared.getTestFilePath(getContext(), "diffmodeltemplates-2.apkg"); imp = new AnkiPackageImporter(dst, tmp); imp.setDupeOnSchemaChange(true); imp.run(); // collection should contain the note we imported assertTrue(dst.noteCount() == 1); // the front template should contain the text added in the 2nd package Long tcid = dst.findCards("").get(0); Note tnote = dst.getCard(tcid).note(); assertTrue(dst.findTemplates(tnote).get(0).getString("qfmt").contains("Changed Front Template")); } public void testAnki2Updates() throws IOException { // create a new empty deck Collection dst = Shared.getEmptyCol(getContext()); String tmp = Shared.getTestFilePath(getContext(), "update1.apkg"); AnkiPackageImporter imp = new AnkiPackageImporter(dst, tmp); imp.run(); assertTrue(imp.getDupes() == 0); assertTrue(imp.getAdded() == 1); assertTrue(imp.getUpdated() == 0); // importing again should be idempotent imp = new AnkiPackageImporter(dst, tmp); imp.run(); assertTrue(imp.getDupes() == 1); assertTrue(imp.getAdded() == 0); assertTrue(imp.getUpdated() == 0); // importing a newer note should update assertTrue(dst.noteCount() == 1); assertTrue(dst.getDb().queryString("select flds from notes").startsWith("hello")); tmp = Shared.getTestFilePath(getContext(), "update2.apkg"); imp = new AnkiPackageImporter(dst, tmp); imp.run(); assertTrue(imp.getDupes()== 1); assertTrue(imp.getAdded() == 0); assertTrue(imp.getUpdated() == 1); assertTrue(dst.getDb().queryString("select flds from notes").startsWith("goodbye")); } // Remove @Suppress when csv importer is implemented @Suppress public void testCsv() throws IOException { Collection deck = Shared.getEmptyCol(getContext()); String file = Shared.getTestFilePath(getContext(), "text-2fields.txt"); TextImporter i = new TextImporter(deck, file); i.initMapping(); i.run(); // four problems - too many & too few fields, a missing front, and a // duplicate entry assertTrue(i.getLog().size() == 5); assertTrue(i.getTotal() == 5); // if we run the import again, it should update instead i.run(); assertTrue(i.getLog().size() == 10); assertTrue(i.getTotal() == 5); // but importing should not clobber tags if they're unmapped Note n = deck.getNote(deck.getDb().queryLongScalar("select id from notes")); n.addTag("test"); n.flush(); i.run(); n.load(); assertTrue((n.getTags().size() == 1) && (n.getTags().get(0) == "test")); // if add-only mode, count will be 0 i.setImportMode(1); i.run(); assertTrue(i.getTotal() == 0); // and if dupes mode, will reimport everything assertTrue(deck.cardCount() == 5); i.setImportMode(2); i.run(); // includes repeated field assertTrue(i.getTotal() == 6); assertTrue(deck.cardCount() == 11); deck.close(); } // Remove @Suppress when csv importer is implemented @Suppress public void testCsv2() throws IOException, ConfirmModSchemaException { Collection deck = Shared.getEmptyCol(getContext()); Models mm = deck.getModels(); JSONObject m = mm.current(); JSONObject f = mm.newField("Three"); mm.addField(m, f); mm.save(m); Note n = deck.newNote(); n.setItem("Front", "1"); n.setItem("Back", "2"); n.setItem("Three", "3"); deck.addNote(n); // an update with unmapped fields should not clobber those fields String file = Shared.getTestFilePath(getContext(), "text-update.txt"); TextImporter i = new TextImporter(deck, file); i.initMapping(); i.run(); n.load(); assertTrue(n.getItem("Front").equals("1")); assertTrue(n.getItem("Back").equals("x")); assertTrue(n.getItem("Three").equals("3")); deck.close(); } /** * Custom tests for AnkiDroid. */ public void testDupeIgnore() throws IOException { // create a new empty deck Collection dst = Shared.getEmptyCol(getContext()); String tmp = Shared.getTestFilePath(getContext(), "update1.apkg"); AnkiPackageImporter imp = new AnkiPackageImporter(dst, tmp); imp.run(); tmp = Shared.getTestFilePath(getContext(), "update3.apkg"); imp = new AnkiPackageImporter(dst, tmp); imp.run(); // there is a dupe, but it was ignored assertTrue(imp.getDupes() == 1); assertTrue(imp.getAdded() == 0); assertTrue(imp.getUpdated() == 0); } }