20
20
from __future__ import print_function
21
21
22
22
import collections
23
+ import json
23
24
import os
24
25
import textwrap
25
26
import numpy as np
@@ -328,12 +329,41 @@ def test_text_array_to_html(self):
328
329
</table>""" )
329
330
self .assertEqual (convert (d3 ), d3_expected )
330
331
332
+ def assertIsActive (self , plugin , expected_finally_is_active ):
333
+ """Helper to simulate threading for asserting on is_active()."""
334
+ patcher = tf .compat .v1 .test .mock .patch ('threading.Thread.start' , autospec = True )
335
+ mock = patcher .start ()
336
+ self .addCleanup (patcher .stop )
337
+
338
+ # Initial response from is_active() is always False.
339
+ self .assertFalse (plugin .is_active ())
340
+ thread = plugin ._index_impl_thread
341
+ mock .assert_called_once_with (thread )
342
+
343
+ # The thread hasn't run yet, so is_active() should still be False, and we
344
+ # should not have tried to launch a second thread.
345
+ self .assertFalse (plugin .is_active ())
346
+ mock .assert_called_once_with (thread )
347
+
348
+ # Run the thread; it should clean up after itself.
349
+ thread .run ()
350
+ self .assertIsNone (plugin ._index_impl_thread )
351
+
352
+ if expected_finally_is_active :
353
+ self .assertTrue (plugin .is_active ())
354
+ # The call above shouldn't have launched a new thread.
355
+ mock .assert_called_once_with (thread )
356
+ else :
357
+ self .assertFalse (plugin .is_active ())
358
+ # The call above should have launched a second thread to check again.
359
+ self .assertEqual (2 , mock .call_count )
360
+
331
361
def testPluginIsActiveWhenNoRuns (self ):
332
362
"""The plugin should be inactive when there are no runs."""
333
363
multiplexer = event_multiplexer .EventMultiplexer ()
334
364
context = base_plugin .TBContext (logdir = self .logdir , multiplexer = multiplexer )
335
365
plugin = text_plugin .TextPlugin (context )
336
- self .assertFalse (plugin . is_active () )
366
+ self .assertIsActive (plugin , False )
337
367
338
368
def testPluginIsActiveWhenTextRuns (self ):
339
369
"""The plugin should be active when there are runs with text."""
@@ -342,7 +372,15 @@ def testPluginIsActiveWhenTextRuns(self):
342
372
plugin = text_plugin .TextPlugin (context )
343
373
multiplexer .AddRunsFromDirectory (self .logdir )
344
374
multiplexer .Reload ()
345
- self .assertTrue (plugin .is_active ())
375
+
376
+ patcher = tf .compat .v1 .test .mock .patch ('threading.Thread.start' , autospec = True )
377
+ mock = patcher .start ()
378
+ self .addCleanup (patcher .stop )
379
+ self .assertTrue (plugin .is_active (), True )
380
+
381
+ # Data is available within the multiplexer. No thread should have started
382
+ # for checking plugin assets data.
383
+ self .assertFalse (mock .called )
346
384
347
385
def testPluginIsActiveWhenRunsButNoText (self ):
348
386
"""The plugin should be inactive when there are runs but none has text."""
@@ -353,13 +391,99 @@ def testPluginIsActiveWhenRunsButNoText(self):
353
391
self .generate_testdata (include_text = False , logdir = logdir )
354
392
multiplexer .AddRunsFromDirectory (logdir )
355
393
multiplexer .Reload ()
356
- self .assertFalse (plugin .is_active ())
394
+ self .assertIsActive (plugin , False )
395
+
396
+ def testPluginTagsImpl (self ):
397
+ patcher = tf .compat .v1 .test .mock .patch ('threading.Thread.start' , autospec = True )
398
+ mock = patcher .start ()
399
+ self .addCleanup (patcher .stop )
357
400
358
- def testPluginIndexImpl (self ):
359
- run_to_tags = self .plugin .index_impl ()
401
+ # Initially, the thread for checking for plugin assets data has not run.
402
+ # Hence, the mapping should only have data from the multiplexer.
403
+ run_to_tags = self .plugin .tags_impl ()
360
404
self .assertItemsEqual (['fry' , 'leela' ], run_to_tags .keys ())
361
405
self .assertItemsEqual (['message' , 'vector' ], run_to_tags ['fry' ])
362
406
self .assertItemsEqual (['message' , 'vector' ], run_to_tags ['leela' ])
407
+ thread = self .plugin ._index_impl_thread
408
+ mock .assert_called_once_with (thread )
409
+
410
+ # The thread hasn't run yet, so no change in response, and we should not
411
+ # have tried to launch a second thread.
412
+ run_to_tags = self .plugin .tags_impl ()
413
+ self .assertItemsEqual (['fry' , 'leela' ], run_to_tags .keys ())
414
+ self .assertItemsEqual (['message' , 'vector' ], run_to_tags ['fry' ])
415
+ self .assertItemsEqual (['message' , 'vector' ], run_to_tags ['leela' ])
416
+ mock .assert_called_once_with (thread )
417
+
418
+ # Run the thread; it should clean up after itself.
419
+ thread .run ()
420
+ self .assertIsNone (self .plugin ._index_impl_thread )
421
+
422
+ # Expect response to be identical to calling index_impl() directly.
423
+ self .assertEqual (self .plugin .index_impl (), self .plugin .tags_impl ())
424
+ # The call above should have launched a second thread to check again.
425
+ self .assertEqual (2 , mock .call_count )
426
+
427
+
428
+ class TextPluginBackwardsCompatibilityTest (tf .test .TestCase ):
429
+
430
+ def setUp (self ):
431
+ self .logdir = self .get_temp_dir ()
432
+ self .generate_testdata ()
433
+ multiplexer = event_multiplexer .EventMultiplexer ()
434
+ multiplexer .AddRunsFromDirectory (self .logdir )
435
+ multiplexer .Reload ()
436
+ context = base_plugin .TBContext (logdir = self .logdir , multiplexer = multiplexer )
437
+ self .plugin = text_plugin .TextPlugin (context )
438
+
439
+ def generate_testdata (self ):
440
+ tf .compat .v1 .reset_default_graph ()
441
+ sess = tf .compat .v1 .Session ()
442
+ placeholder = tf .constant ('I am deprecated.' )
443
+
444
+ # Previously, we had used a means of creating text summaries that used
445
+ # plugin assets (which loaded JSON files containing runs and tags). The
446
+ # plugin must continue to be able to load summaries of that format, so we
447
+ # create a summary using that old plugin asset-based method here.
448
+ plugin_asset_summary = tf .compat .v1 .summary .tensor_summary ('old_plugin_asset_summary' ,
449
+ placeholder )
450
+ assets_directory = os .path .join (self .logdir , 'fry' , 'plugins' ,
451
+ 'tensorboard_text' )
452
+ # Make the directory of assets if it does not exist.
453
+ if not os .path .isdir (assets_directory ):
454
+ try :
455
+ os .makedirs (assets_directory )
456
+ except OSError as err :
457
+ self .assertFail ('Could not make assets directory %r: %r' ,
458
+ assets_directory , err )
459
+ json_path = os .path .join (assets_directory , 'tensors.json' )
460
+ with open (json_path , 'w+' ) as tensors_json_file :
461
+ # Write the op name to a JSON file that the text plugin later uses to
462
+ # determine the tag names of tensors to fetch.
463
+ tensors_json_file .write (json .dumps ([plugin_asset_summary .op .name ]))
464
+
465
+ run_name = 'fry'
466
+ subdir = os .path .join (self .logdir , run_name )
467
+ with test_util .FileWriterCache .get (subdir ) as writer :
468
+ writer .add_graph (sess .graph )
469
+
470
+ summ = sess .run (plugin_asset_summary )
471
+ writer .add_summary (summ )
472
+
473
+ def testIndex (self ):
474
+ index = self .plugin .index_impl ()
475
+ self .assertItemsEqual (['fry' ], index .keys ())
476
+ # The summary made via plugin assets (the old method being phased out) is
477
+ # only available for run 'fry'.
478
+ self .assertItemsEqual (['old_plugin_asset_summary' ],
479
+ index ['fry' ])
480
+
481
+ def testText (self ):
482
+ fry = self .plugin .text_impl ('fry' , 'old_plugin_asset_summary' )
483
+ self .assertEqual (len (fry ), 1 )
484
+ self .assertEqual (fry [0 ]['step' ], 0 )
485
+ self .assertEqual (fry [0 ]['text' ], u'<p>I am deprecated.</p>' )
486
+
363
487
364
488
365
489
if __name__ == '__main__' :
0 commit comments