4 DialCentral - Front end for Google's GoogleVoice service.
5 Copyright (C) 2008 Mark Bergman bergman AT merctech DOT com
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 Lesser General Public License for more details.
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
23 from __future__ import with_statement
42 def getmtime_nothrow(path):
44 return os.path.getmtime(path)
49 def display_error_message(msg):
50 error_dialog = gtk.MessageDialog(None, 0, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, msg)
52 def close(dialog, response):
54 error_dialog.connect("response", close)
58 class Dialcentral(object):
61 os.path.join(os.path.dirname(__file__), "dialcentral.glade"),
62 os.path.join(os.path.dirname(__file__), "../lib/dialcentral.glade"),
63 '/usr/lib/dialcentral/dialcentral.glade',
74 BACKENDS = (NULL_BACKEND, GV_BACKEND)
77 self._initDone = False
78 self._connection = None
80 self._clipboard = gtk.clipboard_get()
82 self._credentials = ("", "")
83 self._selectedBackendId = self.NULL_BACKEND
84 self._defaultBackendId = self.GV_BACKEND
85 self._phoneBackends = None
87 self._accountViews = None
88 self._messagesViews = None
89 self._recentViews = None
90 self._contactsViews = None
91 self._alarmHandler = None
92 self._ledHandler = None
93 self._originalCurrentLabels = []
95 for path in self._glade_files:
96 if os.path.isfile(path):
97 self._widgetTree = gtk.glade.XML(path)
100 display_error_message("Cannot find dialcentral.glade")
104 self._window = self._widgetTree.get_widget("mainWindow")
105 self._notebook = self._widgetTree.get_widget("notebook")
106 self._errorDisplay = gtk_toolbox.ErrorDisplay(self._widgetTree)
107 self._credentialsDialog = gtk_toolbox.LoginWindow(self._widgetTree)
109 self._isFullScreen = False
110 self._app = hildonize.get_app_class()()
111 self._window = hildonize.hildonize_window(self._app, self._window)
112 hildonize.hildonize_text_entry(self._widgetTree.get_widget("usernameentry"))
113 hildonize.hildonize_password_entry(self._widgetTree.get_widget("passwordentry"))
115 for scrollingWidget in (
116 'recent_scrolledwindow',
117 'message_scrolledwindow',
118 'contacts_scrolledwindow',
119 "phoneSelectionMessages_scrolledwindow",
120 "smsMessages_scrolledwindow",
122 hildonize.hildonize_scrollwindow(self._widgetTree.get_widget(scrollingWidget))
123 for scrollingWidget in (
124 "phonetypes_scrolledwindow",
125 "smsMessage_scrolledEntry",
127 hildonize.hildonize_scrollwindow_with_viewport(self._widgetTree.get_widget(scrollingWidget))
129 replacementButtons = [gtk.Button("Test")]
130 menu = hildonize.hildonize_menu(
132 self._widgetTree.get_widget("dialpad_menubar"),
136 self._window.connect("key-press-event", self._on_key_press)
137 self._window.connect("window-state-event", self._on_window_state_change)
138 if not hildonize.IS_HILDON_SUPPORTED:
139 logging.warning("No hildonization support")
141 hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__)
143 self._window.connect("destroy", self._on_close)
144 self._window.set_default_size(800, 300)
145 self._window.show_all()
147 self._loginSink = gtk_toolbox.threaded_stage(
150 gtk_toolbox.null_sink(),
154 backgroundSetup = threading.Thread(target=self._idle_setup)
155 backgroundSetup.setDaemon(True)
156 backgroundSetup.start()
158 def _idle_setup(self):
160 If something can be done after the UI loads, push it here so it's not blocking the UI
162 # Barebones UI handlers
167 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
168 with gtk_toolbox.gtk_lock():
169 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
170 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
171 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
172 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
173 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
175 self._dialpads[self._selectedBackendId].enable()
176 self._accountViews[self._selectedBackendId].enable()
177 self._recentViews[self._selectedBackendId].enable()
178 self._messagesViews[self._selectedBackendId].enable()
179 self._contactsViews[self._selectedBackendId].enable()
181 with gtk_toolbox.gtk_lock():
182 self._errorDisplay.push_exception()
184 # Setup maemo specifics
188 except (ImportError, OSError):
192 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
193 device = osso.DeviceState(self._osso)
194 device.set_device_state_callback(self._on_device_state_change, 0)
196 logging.warning("No device state support")
200 self._alarmHandler = alarm_handler.AlarmHandler()
201 except (ImportError, OSError):
204 with gtk_toolbox.gtk_lock():
205 self._errorDisplay.push_exception()
207 logging.warning("No notification support")
208 if hildonize.IS_HILDON_SUPPORTED:
211 self._ledHandler = led_handler.LedHandler()
213 logging.exception('LED Handling failed: "%s"' % str(e))
214 self._ledHandler = None
216 self._ledHandler = None
220 except (ImportError, OSError):
222 self._connection = None
223 if conic is not None:
224 self._connection = conic.Connection()
225 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
226 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
228 logging.warning("No connection support")
230 with gtk_toolbox.gtk_lock():
231 self._errorDisplay.push_exception()
233 # Setup costly backends
240 os.makedirs(constants._data_path_)
244 gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
246 self._phoneBackends.update({
247 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
249 with gtk_toolbox.gtk_lock():
250 unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay)
251 self._dialpads.update({
252 self.GV_BACKEND: unifiedDialpad,
254 self._accountViews.update({
255 self.GV_BACKEND: gv_views.AccountInfo(
256 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
259 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
260 self._recentViews.update({
261 self.GV_BACKEND: gv_views.RecentCallsView(
262 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
265 self._messagesViews.update({
266 self.GV_BACKEND: gv_views.MessagesView(
267 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
270 self._contactsViews.update({
271 self.GV_BACKEND: gv_views.ContactsView(
272 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
276 fsContactsPath = os.path.join(constants._data_path_, "contacts")
277 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
279 self._dialpads[self.GV_BACKEND].number_selected = self._select_action
280 self._recentViews[self.GV_BACKEND].number_selected = self._select_action
281 self._messagesViews[self.GV_BACKEND].number_selected = self._select_action
282 self._contactsViews[self.GV_BACKEND].number_selected = self._select_action
285 self._phoneBackends[self.GV_BACKEND],
288 mergedBook = gv_views.MergedAddressBook(addressBooks, gv_views.MergedAddressBook.advanced_lastname_sorter)
289 self._contactsViews[self.GV_BACKEND].append(mergedBook)
290 self._contactsViews[self.GV_BACKEND].extend(addressBooks)
291 self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2])
294 "on_paste": self._on_paste,
295 "on_refresh": self._on_menu_refresh,
296 "on_clearcookies_clicked": self._on_clearcookies_clicked,
297 "on_about_activate": self._on_about_activate,
299 if hildonize.GTK_MENU_USED:
300 self._widgetTree.signal_autoconnect(callbackMapping)
301 self._notebook.connect("switch-page", self._on_notebook_switch_page)
302 self._widgetTree.get_widget("clearcookies").connect("clicked", self._on_clearcookies_clicked)
304 with gtk_toolbox.gtk_lock():
305 self._originalCurrentLabels = [
306 self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
307 for pageIndex in xrange(self._notebook.get_n_pages())
309 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
310 self._notebookTapHandler.enable()
311 self._notebookTapHandler.on_tap = self._reset_tab_refresh
312 self._notebookTapHandler.on_hold = self._on_tab_refresh
313 self._notebookTapHandler.on_holding = self._set_tab_refresh
314 self._notebookTapHandler.on_cancel = self._reset_tab_refresh
316 self._initDone = True
318 config = ConfigParser.SafeConfigParser()
319 config.read(constants._user_settings_)
320 with gtk_toolbox.gtk_lock():
321 self.load_settings(config)
323 with gtk_toolbox.gtk_lock():
324 self._errorDisplay.push_exception()
326 self._spawn_attempt_login(2)
328 def _spawn_attempt_login(self, *args):
329 self._loginSink.send(args)
331 def _attempt_login(self, numOfAttempts = 10, force = False):
333 @note This must be run outside of the UI lock
336 assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
337 assert self._initDone, "Attempting login before app is fully loaded"
339 serviceId = self.NULL_BACKEND
343 self.refresh_session()
344 serviceId = self._defaultBackendId
347 logging.exception('Session refresh failed with the following message "%s"' % str(e))
350 loggedIn, serviceId = self._login_by_user(numOfAttempts)
352 with gtk_toolbox.gtk_lock():
353 self._change_loggedin_status(serviceId)
355 hildonize.show_information_banner(self._window, "Logged In")
357 with gtk_toolbox.gtk_lock():
358 self._errorDisplay.push_exception()
360 def refresh_session(self):
362 @note Thread agnostic
364 assert self._initDone, "Attempting login before app is fully loaded"
368 loggedIn = self._login_by_cookie()
370 loggedIn = self._login_by_settings()
373 raise RuntimeError("Login Failed")
375 def _login_by_cookie(self):
377 @note Thread agnostic
379 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
381 logging.info("Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId])
384 def _login_by_settings(self):
386 @note Thread agnostic
388 username, password = self._credentials
389 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
391 self._credentials = username, password
392 logging.info("Logged into %r through settings" % self._phoneBackends[self._defaultBackendId])
395 def _login_by_user(self, numOfAttempts):
397 @note This must be run outside of the UI lock
399 loggedIn, (username, password) = False, self._credentials
400 tmpServiceId = self.GV_BACKEND
401 for attemptCount in xrange(numOfAttempts):
404 with gtk_toolbox.gtk_lock():
405 credentials = self._credentialsDialog.request_credentials(
406 defaultCredentials = self._credentials
408 if not self._phoneBackends[tmpServiceId].get_callback_number():
409 # subtle reminder to the users to configure things
410 self._notebook.set_current_page(self.ACCOUNT_TAB)
411 username, password = credentials
412 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
415 serviceId = tmpServiceId
416 self._credentials = username, password
417 logging.info("Logged into %r through user request" % self._phoneBackends[serviceId])
419 serviceId = self.NULL_BACKEND
420 self._notebook.set_current_page(self.ACCOUNT_TAB)
422 return loggedIn, serviceId
424 def _select_action(self, action, number, message):
425 self.refresh_session()
426 if action == "select":
427 self._dialpads[self._selectedBackendId].set_number(number)
428 self._notebook.set_current_page(self.KEYPAD_TAB)
429 elif action == "dial":
430 self._on_dial_clicked(number)
431 elif action == "sms":
432 self._on_sms_clicked(number, message)
434 assert False, "Unknown action: %s" % action
436 def _change_loggedin_status(self, newStatus):
437 oldStatus = self._selectedBackendId
438 if oldStatus == newStatus:
441 self._dialpads[oldStatus].disable()
442 self._accountViews[oldStatus].disable()
443 self._recentViews[oldStatus].disable()
444 self._messagesViews[oldStatus].disable()
445 self._contactsViews[oldStatus].disable()
447 self._dialpads[newStatus].enable()
448 self._accountViews[newStatus].enable()
449 self._recentViews[newStatus].enable()
450 self._messagesViews[newStatus].enable()
451 self._contactsViews[newStatus].enable()
453 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
454 self._phoneBackends[self._selectedBackendId].set_sane_callback()
456 self._selectedBackendId = newStatus
458 self._accountViews[self._selectedBackendId].update()
459 self._refresh_active_tab()
461 def load_settings(self, config):
466 self._defaultBackendId = config.getint(constants.__pretty_app_name__, "active")
468 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
469 for i in xrange(len(self._credentials))
472 base64.b64decode(blob)
475 self._credentials = tuple(creds)
477 if self._alarmHandler is not None:
478 self._alarmHandler.load_settings(config, "alarm")
479 except ConfigParser.NoOptionError, e:
481 "Settings file %s is missing section %s" % (
482 constants._user_settings_,
486 except ConfigParser.NoSectionError, e:
488 "Settings file %s is missing section %s" % (
489 constants._user_settings_,
494 for backendId, view in itertools.chain(
495 self._dialpads.iteritems(),
496 self._accountViews.iteritems(),
497 self._messagesViews.iteritems(),
498 self._recentViews.iteritems(),
499 self._contactsViews.iteritems(),
501 sectionName = "%s - %s" % (backendId, view.name())
503 view.load_settings(config, sectionName)
504 except ConfigParser.NoOptionError, e:
506 "Settings file %s is missing section %s" % (
507 constants._user_settings_,
511 except ConfigParser.NoSectionError, e:
513 "Settings file %s is missing section %s" % (
514 constants._user_settings_,
520 previousOrientation = config.getint(constants.__pretty_app_name__, "orientation")
521 if previousOrientation == gtk.ORIENTATION_HORIZONTAL:
522 hildonize.window_to_landscape(self._window)
523 elif previousOrientation == gtk.ORIENTATION_VERTICAL:
524 hildonize.window_to_portrait(self._window)
525 except ConfigParser.NoOptionError, e:
527 "Settings file %s is missing section %s" % (
528 constants._user_settings_,
532 except ConfigParser.NoSectionError, e:
534 "Settings file %s is missing section %s" % (
535 constants._user_settings_,
540 def save_settings(self, config):
542 @note Thread Agnostic
544 config.add_section(constants.__pretty_app_name__)
545 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
546 config.set(constants.__pretty_app_name__, "orientation", str(int(gtk_toolbox.get_screen_orientation())))
547 for i, value in enumerate(self._credentials):
548 blob = base64.b64encode(value)
549 config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
550 config.add_section("alarm")
551 if self._alarmHandler is not None:
552 self._alarmHandler.save_settings(config, "alarm")
554 for backendId, view in itertools.chain(
555 self._dialpads.iteritems(),
556 self._accountViews.iteritems(),
557 self._messagesViews.iteritems(),
558 self._recentViews.iteritems(),
559 self._contactsViews.iteritems(),
561 sectionName = "%s - %s" % (backendId, view.name())
562 config.add_section(sectionName)
563 view.save_settings(config, sectionName)
565 def _save_settings(self):
567 @note Thread Agnostic
569 config = ConfigParser.SafeConfigParser()
570 self.save_settings(config)
571 with open(constants._user_settings_, "wb") as configFile:
572 config.write(configFile)
574 def _refresh_active_tab(self):
575 pageIndex = self._notebook.get_current_page()
576 if pageIndex == self.CONTACTS_TAB:
577 self._contactsViews[self._selectedBackendId].update(force=True)
578 elif pageIndex == self.RECENT_TAB:
579 self._recentViews[self._selectedBackendId].update(force=True)
580 elif pageIndex == self.MESSAGES_TAB:
581 self._messagesViews[self._selectedBackendId].update(force=True)
583 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
584 if self._ledHandler is not None:
585 self._ledHandler.off()
587 def _on_close(self, *args, **kwds):
589 if self._osso is not None:
593 self._save_settings()
597 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
599 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
600 For system_inactivity, we have no background tasks to pause
602 @note Hildon specific
606 for backendId in self.BACKENDS:
607 self._phoneBackends[backendId].clear_caches()
608 self._contactsViews[self._selectedBackendId].clear_caches()
611 if save_unsaved_data or shutdown:
612 self._save_settings()
614 self._errorDisplay.push_exception()
616 def _on_connection_change(self, connection, event, magicIdentifier):
618 @note Hildon specific
623 status = event.get_status()
624 error = event.get_error()
625 iap_id = event.get_iap_id()
626 bearer = event.get_bearer_type()
628 if status == conic.STATUS_CONNECTED:
630 self._spawn_attempt_login(2)
631 elif status == conic.STATUS_DISCONNECTED:
633 self._defaultBackendId = self._selectedBackendId
634 self._change_loggedin_status(self.NULL_BACKEND)
636 self._errorDisplay.push_exception()
638 def _on_window_state_change(self, widget, event, *args):
640 @note Hildon specific
643 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
644 self._isFullScreen = True
646 self._isFullScreen = False
648 self._errorDisplay.push_exception()
650 def _on_key_press(self, widget, event, *args):
652 @note Hildon specific
656 event.keyval == gtk.keysyms.F6 or
657 event.keyval == gtk.keysyms.Return and event.get_state() & gtk.gdk.CONTROL_MASK
659 if self._isFullScreen:
660 self._window.unfullscreen()
662 self._window.fullscreen()
664 self._errorDisplay.push_exception()
666 def _on_clearcookies_clicked(self, *args):
668 self._phoneBackends[self._selectedBackendId].logout()
669 self._accountViews[self._selectedBackendId].clear()
670 self._recentViews[self._selectedBackendId].clear()
671 self._messagesViews[self._selectedBackendId].clear()
672 self._contactsViews[self._selectedBackendId].clear()
673 self._change_loggedin_status(self.NULL_BACKEND)
675 self._spawn_attempt_login(2, True)
677 self._errorDisplay.push_exception()
679 def _on_notebook_switch_page(self, notebook, page, pageIndex):
681 self._reset_tab_refresh()
683 didRecentUpdate = False
684 didMessagesUpdate = False
686 if pageIndex == self.RECENT_TAB:
687 didRecentUpdate = self._recentViews[self._selectedBackendId].update()
688 elif pageIndex == self.MESSAGES_TAB:
689 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
690 elif pageIndex == self.CONTACTS_TAB:
691 self._contactsViews[self._selectedBackendId].update()
692 elif pageIndex == self.ACCOUNT_TAB:
693 self._accountViews[self._selectedBackendId].update()
695 if didRecentUpdate or didMessagesUpdate:
696 if self._ledHandler is not None:
697 self._ledHandler.off()
699 self._errorDisplay.push_exception()
701 def _set_tab_refresh(self, *args):
703 pageIndex = self._notebook.get_current_page()
704 child = self._notebook.get_nth_page(pageIndex)
705 self._notebook.get_tab_label(child).set_text("Refresh?")
707 self._errorDisplay.push_exception()
710 def _reset_tab_refresh(self, *args):
712 pageIndex = self._notebook.get_current_page()
713 child = self._notebook.get_nth_page(pageIndex)
714 self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
716 self._errorDisplay.push_exception()
719 def _on_tab_refresh(self, *args):
721 self._refresh_active_tab()
722 self._reset_tab_refresh()
724 self._errorDisplay.push_exception()
727 def _on_sms_clicked(self, number, message):
729 assert number, "No number specified"
730 assert message, "Empty message"
732 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
735 self._errorDisplay.push_exception()
739 self._errorDisplay.push_message(
740 "Backend link with GoogleVoice is not working, please try again"
746 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
747 hildonize.show_information_banner(self._window, "Sending to %s" % number)
750 self._errorDisplay.push_exception()
753 self._dialpads[self._selectedBackendId].clear()
755 self._errorDisplay.push_exception()
757 def _on_dial_clicked(self, number):
759 assert number, "No number to call"
761 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
764 self._errorDisplay.push_exception()
768 self._errorDisplay.push_message(
769 "Backend link with GoogleVoice is not working, please try again"
775 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
776 self._phoneBackends[self._selectedBackendId].dial(number)
777 hildonize.show_information_banner(self._window, "Calling %s" % number)
780 self._errorDisplay.push_exception()
783 self._dialpads[self._selectedBackendId].clear()
785 self._errorDisplay.push_exception()
787 def _on_menu_refresh(self, *args):
789 self._refresh_active_tab()
791 self._errorDisplay.push_exception()
793 def _on_paste(self, *args):
795 contents = self._clipboard.wait_for_text()
796 if contents is not None:
797 self._dialpads[self._selectedBackendId].set_number(contents)
799 self._errorDisplay.push_exception()
801 def _on_about_activate(self, *args):
803 dlg = gtk.AboutDialog()
804 dlg.set_name(constants.__pretty_app_name__)
805 dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
806 dlg.set_copyright("Copyright 2008 - LGPL")
807 dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice account. This application is not affiliated with Google in any way")
808 dlg.set_website("http://gc-dialer.garage.maemo.org/")
809 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
813 self._errorDisplay.push_exception()
819 failureCount, testCount = doctest.testmod()
821 print "Tests Successful"
828 _lock_file = os.path.join(constants._data_path_, ".lock")
829 #with gtk_toolbox.flock(_lock_file, 0):
830 gtk.gdk.threads_init()
832 if hildonize.IS_HILDON_SUPPORTED:
833 gtk.set_application_name(constants.__pretty_app_name__)
834 handle = Dialcentral()
838 class DummyOptions(object):
844 if __name__ == "__main__":
845 logging.basicConfig(level=logging.DEBUG)
847 if len(sys.argv) > 1:
853 if optparse is not None:
854 parser = optparse.OptionParser()
855 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
856 (commandOptions, commandArgs) = parser.parse_args()
858 commandOptions = DummyOptions()
861 if commandOptions.test: