Skip to content

Commit 3e7ad1c

Browse files
committed
tests for thumbnail tasks
1 parent a031cf4 commit 3e7ad1c

7 files changed

Lines changed: 267 additions & 26 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package org.thoughtcrime.securesms.database;
2+
3+
import android.net.Uri;
4+
import android.test.AndroidTestCase;
5+
6+
import org.thoughtcrime.securesms.crypto.MasterSecret;
7+
import org.thoughtcrime.securesms.jobs.ThumbnailGenerateJob;
8+
9+
import java.io.InputStream;
10+
import java.util.HashSet;
11+
import java.util.Set;
12+
13+
import ws.com.google.android.mms.pdu.PduPart;
14+
15+
import static org.mockito.Matchers.any;
16+
import static org.mockito.Matchers.anyFloat;
17+
import static org.mockito.Matchers.anyLong;
18+
import static org.mockito.Matchers.eq;
19+
import static org.mockito.Mockito.doCallRealMethod;
20+
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.never;
22+
import static org.mockito.Mockito.times;
23+
import static org.mockito.Mockito.verify;
24+
import static org.mockito.Mockito.when;
25+
import static org.thoughtcrime.securesms.jobs.ThumbnailGenerateJobTest.getThumbnailGenerateJob;
26+
27+
public class PartDatabaseTest extends AndroidTestCase {
28+
private static final long PART_ID = 1L;
29+
30+
private PartDatabase database;
31+
private Set<Long> tasks = new HashSet<>();
32+
33+
@Override
34+
public void setUp() {
35+
database = mock(PartDatabase.class);
36+
}
37+
38+
public void testThumbnailStreamExistsCase() throws Exception {
39+
when(database.getDataStream(any(MasterSecret.class), anyLong(), eq("thumbnail"))).thenReturn(mock(InputStream.class));
40+
doCallRealMethod().when(database).getThumbnailStream(any(MasterSecret.class), anyLong());
41+
42+
assertNotNull(database.getThumbnailStream(null, PART_ID));
43+
44+
verify(database, never()).getThumbnailGenerateJob(anyLong());
45+
verify(database, never()).updatePartThumbnail(any(MasterSecret.class), anyLong(), any(PduPart.class), any(InputStream.class), anyFloat());
46+
verify(database, never()).markThumbnailTaskStarted(eq(PART_ID));
47+
}
48+
49+
public void testThumbnailStreamBlocksOnRunningJob() throws Exception {
50+
when(database.getPart(eq(PART_ID))).thenReturn(getPduPartSkeleton("x/x"));
51+
52+
when(database.getDataStream(any(MasterSecret.class), anyLong(), eq("thumbnail"))).thenReturn(null);
53+
when(database.getThumbnailTasks()).thenReturn(tasks);
54+
55+
doCallRealMethod().when(database).markThumbnailTaskStarted(anyLong());
56+
doCallRealMethod().when(database).markThumbnailTaskEnded(anyLong());
57+
doCallRealMethod().when(database).getThumbnailStream(any(MasterSecret.class), anyLong());
58+
59+
final ThumbnailGenerateJob job = getThumbnailGenerateJob(getContext(), database);
60+
job.onAdded();
61+
new Thread(new Runnable() {
62+
@Override
63+
public void run() {
64+
try {
65+
Thread.sleep(1000L);
66+
job.onRun(null);
67+
} catch (Exception e) {
68+
throw new AssertionError("interrupted");
69+
}
70+
}
71+
}).start();
72+
database.getThumbnailStream(null, 1);
73+
74+
verify(database, times(3)).getDataStream(any(MasterSecret.class), anyLong(), eq("thumbnail"));
75+
verify(database, never()).getThumbnailGenerateJob(anyLong());
76+
verify(database, never()).updatePartThumbnail(any(MasterSecret.class), anyLong(), any(PduPart.class), any(InputStream.class), anyFloat());
77+
verify(database, times(1)).markThumbnailTaskStarted(eq(PART_ID));
78+
verify(database, times(1)).markThumbnailTaskEnded(eq(PART_ID));
79+
}
80+
81+
public void testThumbnailStreamGeneratesWhenMissing() throws Exception {
82+
final PartDatabase database = mock(PartDatabase.class);
83+
final Set<Long> tasks = new HashSet<>();
84+
85+
when(database.getPart(PART_ID)).thenReturn(getPduPartSkeleton("x/x"));
86+
87+
when(database.getThumbnailGenerateJob(anyLong())).thenReturn(getThumbnailGenerateJob(getContext(), database));
88+
when(database.getDataStream(any(MasterSecret.class), anyLong(), eq("thumbnail"))).thenReturn(null);
89+
when(database.getThumbnailTasks()).thenReturn(tasks);
90+
91+
doCallRealMethod().when(database).markThumbnailTaskStarted(anyLong());
92+
doCallRealMethod().when(database).markThumbnailTaskEnded(anyLong());
93+
doCallRealMethod().when(database).getThumbnailStream(any(MasterSecret.class), anyLong());
94+
95+
database.getThumbnailStream(null, PART_ID);
96+
97+
verify(database, times(3)).getDataStream(any(MasterSecret.class), anyLong(), eq("thumbnail"));
98+
verify(database, times(1)).getThumbnailGenerateJob(anyLong());
99+
verify(database, times(1)).markThumbnailTaskStarted(eq(PART_ID));
100+
verify(database, times(1)).markThumbnailTaskEnded(eq(PART_ID));
101+
}
102+
103+
private PduPart getPduPartSkeleton(String contentType) {
104+
PduPart part = new PduPart();
105+
part.setContentType(contentType.getBytes());
106+
part.setDataUri(Uri.EMPTY);
107+
return part;
108+
}
109+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package org.thoughtcrime.securesms.jobs;
2+
3+
import android.content.Context;
4+
import android.net.Uri;
5+
import android.test.AndroidTestCase;
6+
7+
import org.thoughtcrime.securesms.crypto.MasterSecret;
8+
import org.thoughtcrime.securesms.database.PartDatabase;
9+
10+
import java.io.FileNotFoundException;
11+
import java.io.InputStream;
12+
13+
import dagger.Module;
14+
import dagger.ObjectGraph;
15+
import dagger.Provides;
16+
import ws.com.google.android.mms.pdu.PduPart;
17+
18+
import static org.mockito.Matchers.any;
19+
import static org.mockito.Matchers.anyFloat;
20+
import static org.mockito.Matchers.anyLong;
21+
import static org.mockito.Matchers.eq;
22+
import static org.mockito.Mockito.mock;
23+
import static org.mockito.Mockito.never;
24+
import static org.mockito.Mockito.verify;
25+
import static org.mockito.Mockito.when;
26+
27+
public class ThumbnailGenerateJobTest extends AndroidTestCase {
28+
private static final long PART_ID = 1L;
29+
30+
private PartDatabase database;
31+
32+
@Override
33+
public void setUp() {
34+
database = mock(PartDatabase.class);
35+
}
36+
37+
public void testTaskAddedRemoved() throws Exception {
38+
ThumbnailGenerateJob job = getThumbnailGenerateJob(getContext(), database);
39+
when(database.getPart(PART_ID)).thenReturn(getPduPartSkeleton("x/x"));
40+
41+
job.onAdded();
42+
verify(database).markThumbnailTaskStarted(eq(PART_ID));
43+
44+
job.onRun(null);
45+
verify(database, never()).updatePartThumbnail(any(MasterSecret.class), anyLong(), any(PduPart.class), any(InputStream.class), anyFloat());
46+
verify(database).markThumbnailTaskEnded(eq(PART_ID));
47+
}
48+
49+
public void testTaskResizesImage() throws Exception {
50+
ThumbnailGenerateJob job = getThumbnailGenerateJob(getContext(), database);
51+
when(database.getPart(PART_ID)).thenReturn(getPduPartSkeleton("image/png"));
52+
53+
job.onAdded();
54+
verify(database).markThumbnailTaskStarted(eq(PART_ID));
55+
56+
try {
57+
job.onRun(null);
58+
throw new AssertionError("should have thrown FNFE as it tried to resize an image");
59+
} catch (FileNotFoundException fnfe) {
60+
// success
61+
}
62+
verify(database).markThumbnailTaskEnded(eq(PART_ID));
63+
}
64+
65+
private PduPart getPduPartSkeleton(String contentType) {
66+
PduPart part = new PduPart();
67+
part.setContentType(contentType.getBytes());
68+
part.setDataUri(Uri.EMPTY);
69+
return part;
70+
}
71+
72+
public static ThumbnailGenerateJob getThumbnailGenerateJob(Context context, PartDatabase database) {
73+
ThumbnailGenerateJob job = new ThumbnailGenerateJob(context, PART_ID);
74+
ObjectGraph objectGraph = ObjectGraph.create(new ThumbnailGenerateJobTest.DatabaseModule(database));
75+
objectGraph.inject(job);
76+
return job;
77+
}
78+
79+
@SuppressWarnings("unused")
80+
@Module(injects = ThumbnailGenerateJob.class)
81+
public static class DatabaseModule {
82+
83+
private final PartDatabase partDatabase;
84+
85+
public DatabaseModule(PartDatabase partDatabase) {
86+
this.partDatabase = partDatabase;
87+
}
88+
89+
@Provides PartDatabase providePartDatabase() {
90+
return partDatabase;
91+
}
92+
}
93+
94+
}

src/org/thoughtcrime/securesms/ApplicationContext.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.thoughtcrime.securesms.crypto.PRNGFixes;
2323
import org.thoughtcrime.securesms.dependencies.AxolotlStorageModule;
24+
import org.thoughtcrime.securesms.dependencies.DatabaseModule;
2425
import org.thoughtcrime.securesms.dependencies.InjectableType;
2526
import org.thoughtcrime.securesms.dependencies.TextSecureCommunicationModule;
2627
import org.thoughtcrime.securesms.jobs.GcmRefreshJob;
@@ -92,7 +93,8 @@ private void initializeJobManager() {
9293

9394
private void initializeDependencyInjection() {
9495
this.objectGraph = ObjectGraph.create(new TextSecureCommunicationModule(this),
95-
new AxolotlStorageModule(this));
96+
new AxolotlStorageModule(this),
97+
new DatabaseModule(this));
9698
}
9799

98100
private void initializeGcmCheck() {

src/org/thoughtcrime/securesms/database/PartDatabase.java

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.thoughtcrime.securesms.jobs.ThumbnailGenerateJob;
3434
import org.thoughtcrime.securesms.mms.PartAuthority;
3535
import org.thoughtcrime.securesms.util.Util;
36+
import org.thoughtcrime.securesms.util.VisibleForTesting;
3637

3738
import java.io.ByteArrayInputStream;
3839
import java.io.File;
@@ -315,7 +316,7 @@ protected OutputStream getPartOutputStream(MasterSecret masterSecret, File path,
315316
return new EncryptingPartOutputStream(path, masterSecret);
316317
}
317318

318-
private InputStream getDataStream(MasterSecret masterSecret, long partId, String dataType)
319+
@VisibleForTesting InputStream getDataStream(MasterSecret masterSecret, long partId, String dataType)
319320
throws FileNotFoundException
320321
{
321322
SQLiteDatabase database = databaseHelper.getReadableDatabase();
@@ -374,15 +375,8 @@ private Pair<File, Long> writePartData(MasterSecret masterSecret, PduPart part)
374375
}
375376
}
376377

377-
private void waitForThumbnailTask(long partId) {
378-
Log.w(TAG, "waiting on thumbnail task for " + partId);
379-
while (thumbnailTasks.contains(partId)) {
380-
try {
381-
thumbnailTasks.wait();
382-
} catch (InterruptedException ie) {
383-
throw new AssertionError("thread interrupted");
384-
}
385-
}
378+
@VisibleForTesting ThumbnailGenerateJob getThumbnailGenerateJob(long partId) {
379+
return new ThumbnailGenerateJob(context, partId);
386380
}
387381

388382
public InputStream getThumbnailStream(MasterSecret masterSecret, long partId) throws FileNotFoundException {
@@ -392,23 +386,26 @@ public InputStream getThumbnailStream(MasterSecret masterSecret, long partId) th
392386
return dataStream;
393387
}
394388

395-
synchronized (thumbnailTasks) {
389+
synchronized (getThumbnailTasks()) {
396390
InputStream stream = getDataStream(masterSecret, partId, THUMBNAIL);
397391
if (stream != null) {
398392
return stream;
399393
}
400394

401-
if (!thumbnailTasks.contains(partId)) {
395+
if (!getThumbnailTasks().contains(partId)) {
402396
try {
403-
ThumbnailGenerateJob job = new ThumbnailGenerateJob(context, partId);
397+
ThumbnailGenerateJob job = getThumbnailGenerateJob(partId);
404398
job.onAdded();
405399
job.onRun(masterSecret);
406400
} catch (MmsException | IOException e) {
407401
Log.w(TAG, e);
408402
throw new FileNotFoundException(e.getMessage());
409403
}
404+
} else {
405+
while (getThumbnailTasks().contains(partId)) {
406+
Util.wait(getThumbnailTasks());
407+
}
410408
}
411-
waitForThumbnailTask(partId);
412409
}
413410

414411
return getDataStream(masterSecret, partId, THUMBNAIL);
@@ -489,16 +486,20 @@ public void updatePartThumbnail(MasterSecret masterSecret, long partId, PduPart
489486
database.update(TABLE_NAME, values, ID_WHERE, new String[]{partId+""});
490487
}
491488

489+
@VisibleForTesting Set<Long> getThumbnailTasks() {
490+
return thumbnailTasks;
491+
}
492+
492493
public void markThumbnailTaskStarted(long partId) {
493-
synchronized (thumbnailTasks) {
494-
thumbnailTasks.add(partId);
494+
synchronized (getThumbnailTasks()) {
495+
getThumbnailTasks().add(partId);
495496
}
496497
}
497498

498499
public void markThumbnailTaskEnded(long partId) {
499-
synchronized (thumbnailTasks) {
500-
thumbnailTasks.remove(partId);
501-
thumbnailTasks.notifyAll();
500+
synchronized (getThumbnailTasks()) {
501+
getThumbnailTasks().remove(partId);
502+
getThumbnailTasks().notifyAll();
502503
}
503504
}
504505
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package org.thoughtcrime.securesms.dependencies;
2+
3+
import android.content.Context;
4+
5+
import org.thoughtcrime.securesms.database.DatabaseFactory;
6+
import org.thoughtcrime.securesms.database.PartDatabase;
7+
import org.thoughtcrime.securesms.jobs.ThumbnailGenerateJob;
8+
9+
import dagger.Module;
10+
import dagger.Provides;
11+
12+
@Module(injects = {ThumbnailGenerateJob.class})
13+
public class DatabaseModule {
14+
15+
private final Context context;
16+
17+
public DatabaseModule(Context context) {
18+
this.context = context;
19+
}
20+
21+
@Provides PartDatabase providePartDatabase() {
22+
return DatabaseFactory.getPartDatabase(context);
23+
}
24+
}

0 commit comments

Comments
 (0)