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"))
114 hildonize.hildonize_combo_entry(self._widgetTree.get_widget("callbackcombo").get_child())
116 for scrollingWidget in (
117 'recent_scrolledwindow',
118 'message_scrolledwindow',
119 'contacts_scrolledwindow',
120 "phoneSelectionMessages_scrolledwindow",
121 "smsMessages_scrolledwindow",
123 hildonize.hildonize_scrollwindow(self._widgetTree.get_widget(scrollingWidget))
124 for scrollingWidget in (
125 "phonetypes_scrolledwindow",
126 "smsMessage_scrolledEntry",
128 hildonize.hildonize_scrollwindow_with_viewport(self._widgetTree.get_widget(scrollingWidget))
130 replacementButtons = [gtk.Button("Test")]
131 menu = hildonize.hildonize_menu(
133 self._widgetTree.get_widget("dialpad_menubar"),
137 if hildonize.IS_HILDON_SUPPORTED:
138 self._window.connect("key-press-event", self._on_key_press)
139 self._window.connect("window-state-event", self._on_window_state_change)
141 logging.warning("No hildonization support")
143 hildonize.set_application_title(self._window, "%s" % constants.__pretty_app_name__)
145 self._window.connect("destroy", self._on_close)
146 self._window.set_default_size(800, 300)
147 self._window.show_all()
149 self._loginSink = gtk_toolbox.threaded_stage(
152 gtk_toolbox.null_sink(),
156 backgroundSetup = threading.Thread(target=self._idle_setup)
157 backgroundSetup.setDaemon(True)
158 backgroundSetup.start()
160 def _idle_setup(self):
162 If something can be done after the UI loads, push it here so it's not blocking the UI
164 # Barebones UI handlers
169 self._phoneBackends = {self.NULL_BACKEND: null_backend.NullDialer()}
170 with gtk_toolbox.gtk_lock():
171 self._dialpads = {self.NULL_BACKEND: null_views.Dialpad(self._widgetTree)}
172 self._accountViews = {self.NULL_BACKEND: null_views.AccountInfo(self._widgetTree)}
173 self._recentViews = {self.NULL_BACKEND: null_views.RecentCallsView(self._widgetTree)}
174 self._messagesViews = {self.NULL_BACKEND: null_views.MessagesView(self._widgetTree)}
175 self._contactsViews = {self.NULL_BACKEND: null_views.ContactsView(self._widgetTree)}
177 self._dialpads[self._selectedBackendId].enable()
178 self._accountViews[self._selectedBackendId].enable()
179 self._recentViews[self._selectedBackendId].enable()
180 self._messagesViews[self._selectedBackendId].enable()
181 self._contactsViews[self._selectedBackendId].enable()
183 with gtk_toolbox.gtk_lock():
184 self._errorDisplay.push_exception()
186 # Setup maemo specifics
190 except (ImportError, OSError):
194 self._osso = osso.Context(constants.__app_name__, constants.__version__, False)
195 device = osso.DeviceState(self._osso)
196 device.set_device_state_callback(self._on_device_state_change, 0)
198 logging.warning("No device state support")
202 self._alarmHandler = alarm_handler.AlarmHandler()
203 except (ImportError, OSError):
206 with gtk_toolbox.gtk_lock():
207 self._errorDisplay.push_exception()
209 logging.warning("No notification support")
210 if hildonize.IS_HILDON_SUPPORTED:
213 self._ledHandler = led_handler.LedHandler()
215 logging.exception('LED Handling failed: "%s"' % str(e))
216 self._ledHandler = None
218 self._ledHandler = None
222 except (ImportError, OSError):
224 self._connection = None
225 if conic is not None:
226 self._connection = conic.Connection()
227 self._connection.connect("connection-event", self._on_connection_change, constants.__app_magic__)
228 self._connection.request_connection(conic.CONNECT_FLAG_NONE)
230 logging.warning("No connection support")
232 with gtk_toolbox.gtk_lock():
233 self._errorDisplay.push_exception()
235 # Setup costly backends
242 os.makedirs(constants._data_path_)
246 gvCookiePath = os.path.join(constants._data_path_, "gv_cookies.txt")
248 self._phoneBackends.update({
249 self.GV_BACKEND: gv_backend.GVDialer(gvCookiePath),
251 with gtk_toolbox.gtk_lock():
252 unifiedDialpad = gv_views.Dialpad(self._widgetTree, self._errorDisplay)
253 self._dialpads.update({
254 self.GV_BACKEND: unifiedDialpad,
256 self._accountViews.update({
257 self.GV_BACKEND: gv_views.AccountInfo(
258 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._alarmHandler, self._errorDisplay
261 self._accountViews[self.GV_BACKEND].save_everything = self._save_settings
262 self._recentViews.update({
263 self.GV_BACKEND: gv_views.RecentCallsView(
264 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
267 self._messagesViews.update({
268 self.GV_BACKEND: gv_views.MessagesView(
269 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
272 self._contactsViews.update({
273 self.GV_BACKEND: gv_views.ContactsView(
274 self._widgetTree, self._phoneBackends[self.GV_BACKEND], self._errorDisplay
278 fsContactsPath = os.path.join(constants._data_path_, "contacts")
279 fileBackend = file_backend.FilesystemAddressBookFactory(fsContactsPath)
281 self._dialpads[self.GV_BACKEND].number_selected = self._select_action
282 self._recentViews[self.GV_BACKEND].number_selected = self._select_action
283 self._messagesViews[self.GV_BACKEND].number_selected = self._select_action
284 self._contactsViews[self.GV_BACKEND].number_selected = self._select_action
287 self._phoneBackends[self.GV_BACKEND],
290 mergedBook = gv_views.MergedAddressBook(addressBooks, gv_views.MergedAddressBook.advanced_lastname_sorter)
291 self._contactsViews[self.GV_BACKEND].append(mergedBook)
292 self._contactsViews[self.GV_BACKEND].extend(addressBooks)
293 self._contactsViews[self.GV_BACKEND].open_addressbook(*self._contactsViews[self.GV_BACKEND].get_addressbooks().next()[0][0:2])
296 "on_paste": self._on_paste,
297 "on_refresh": self._on_menu_refresh,
298 "on_clearcookies_clicked": self._on_clearcookies_clicked,
299 "on_about_activate": self._on_about_activate,
301 if hildonize.GTK_MENU_USED:
302 self._widgetTree.signal_autoconnect(callbackMapping)
303 self._notebook.connect("switch-page", self._on_notebook_switch_page)
304 self._widgetTree.get_widget("clearcookies").connect("clicked", self._on_clearcookies_clicked)
306 with gtk_toolbox.gtk_lock():
307 self._originalCurrentLabels = [
308 self._notebook.get_tab_label(self._notebook.get_nth_page(pageIndex)).get_text()
309 for pageIndex in xrange(self._notebook.get_n_pages())
311 self._notebookTapHandler = gtk_toolbox.TapOrHold(self._notebook)
312 self._notebookTapHandler.enable()
313 self._notebookTapHandler.on_tap = self._reset_tab_refresh
314 self._notebookTapHandler.on_hold = self._on_tab_refresh
315 self._notebookTapHandler.on_holding = self._set_tab_refresh
316 self._notebookTapHandler.on_cancel = self._reset_tab_refresh
318 self._initDone = True
320 config = ConfigParser.SafeConfigParser()
321 config.read(constants._user_settings_)
322 with gtk_toolbox.gtk_lock():
323 self.load_settings(config)
325 with gtk_toolbox.gtk_lock():
326 self._errorDisplay.push_exception()
328 self._spawn_attempt_login(2)
330 def _spawn_attempt_login(self, *args):
331 self._loginSink.send(args)
333 def _attempt_login(self, numOfAttempts = 10, force = False):
335 @note This must be run outside of the UI lock
338 assert 0 <= numOfAttempts, "That was pointless having 0 or less login attempts"
339 assert self._initDone, "Attempting login before app is fully loaded"
341 serviceId = self.NULL_BACKEND
345 self.refresh_session()
346 serviceId = self._defaultBackendId
349 logging.exception('Session refresh failed with the following message "%s"' % str(e))
352 loggedIn, serviceId = self._login_by_user(numOfAttempts)
354 with gtk_toolbox.gtk_lock():
355 self._change_loggedin_status(serviceId)
357 hildonize.show_information_banner(self._window, "Logged In")
359 with gtk_toolbox.gtk_lock():
360 self._errorDisplay.push_exception()
362 def refresh_session(self):
364 @note Thread agnostic
366 assert self._initDone, "Attempting login before app is fully loaded"
370 loggedIn = self._login_by_cookie()
372 loggedIn = self._login_by_settings()
375 raise RuntimeError("Login Failed")
377 def _login_by_cookie(self):
379 @note Thread agnostic
381 loggedIn = self._phoneBackends[self._defaultBackendId].is_authed()
383 logging.info("Logged into %r through cookies" % self._phoneBackends[self._defaultBackendId])
386 def _login_by_settings(self):
388 @note Thread agnostic
390 username, password = self._credentials
391 loggedIn = self._phoneBackends[self._defaultBackendId].login(username, password)
393 self._credentials = username, password
394 logging.info("Logged into %r through settings" % self._phoneBackends[self._defaultBackendId])
397 def _login_by_user(self, numOfAttempts):
399 @note This must be run outside of the UI lock
401 loggedIn, (username, password) = False, self._credentials
402 tmpServiceId = self.GV_BACKEND
403 for attemptCount in xrange(numOfAttempts):
406 with gtk_toolbox.gtk_lock():
407 credentials = self._credentialsDialog.request_credentials(
408 defaultCredentials = self._credentials
410 if not self._phoneBackends[tmpServiceId].get_callback_number():
411 # subtle reminder to the users to configure things
412 self._notebook.set_current_page(self.ACCOUNT_TAB)
413 username, password = credentials
414 loggedIn = self._phoneBackends[tmpServiceId].login(username, password)
417 serviceId = tmpServiceId
418 self._credentials = username, password
419 logging.info("Logged into %r through user request" % self._phoneBackends[serviceId])
421 serviceId = self.NULL_BACKEND
422 self._notebook.set_current_page(self.ACCOUNT_TAB)
424 return loggedIn, serviceId
426 def _select_action(self, action, number, message):
427 self.refresh_session()
428 if action == "select":
429 self._dialpads[self._selectedBackendId].set_number(number)
430 self._notebook.set_current_page(self.KEYPAD_TAB)
431 elif action == "dial":
432 self._on_dial_clicked(number)
433 elif action == "sms":
434 self._on_sms_clicked(number, message)
436 assert False, "Unknown action: %s" % action
438 def _change_loggedin_status(self, newStatus):
439 oldStatus = self._selectedBackendId
440 if oldStatus == newStatus:
443 self._dialpads[oldStatus].disable()
444 self._accountViews[oldStatus].disable()
445 self._recentViews[oldStatus].disable()
446 self._messagesViews[oldStatus].disable()
447 self._contactsViews[oldStatus].disable()
449 self._dialpads[newStatus].enable()
450 self._accountViews[newStatus].enable()
451 self._recentViews[newStatus].enable()
452 self._messagesViews[newStatus].enable()
453 self._contactsViews[newStatus].enable()
455 if self._phoneBackends[self._selectedBackendId].get_callback_number() is None:
456 self._phoneBackends[self._selectedBackendId].set_sane_callback()
458 self._selectedBackendId = newStatus
460 self._accountViews[self._selectedBackendId].update()
461 self._refresh_active_tab()
463 def load_settings(self, config):
468 self._defaultBackendId = config.getint(constants.__pretty_app_name__, "active")
470 config.get(constants.__pretty_app_name__, "bin_blob_%i" % i)
471 for i in xrange(len(self._credentials))
474 base64.b64decode(blob)
477 self._credentials = tuple(creds)
479 if self._alarmHandler is not None:
480 self._alarmHandler.load_settings(config, "alarm")
481 except ConfigParser.NoOptionError, e:
483 "Settings file %s is missing section %s" % (
484 constants._user_settings_,
488 except ConfigParser.NoSectionError, e:
490 "Settings file %s is missing section %s" % (
491 constants._user_settings_,
496 for backendId, view in itertools.chain(
497 self._dialpads.iteritems(),
498 self._accountViews.iteritems(),
499 self._messagesViews.iteritems(),
500 self._recentViews.iteritems(),
501 self._contactsViews.iteritems(),
503 sectionName = "%s - %s" % (backendId, view.name())
505 view.load_settings(config, sectionName)
506 except ConfigParser.NoOptionError, e:
508 "Settings file %s is missing section %s" % (
509 constants._user_settings_,
513 except ConfigParser.NoSectionError, e:
515 "Settings file %s is missing section %s" % (
516 constants._user_settings_,
522 previousOrientation = config.getint(constants.__pretty_app_name__, "orientation")
523 if previousOrientation == gtk.ORIENTATION_HORIZONTAL:
524 hildonize.window_to_landscape(self._window)
525 elif previousOrientation == gtk.ORIENTATION_VERTICAL:
526 hildonize.window_to_portrait(self._window)
527 except ConfigParser.NoOptionError, e:
529 "Settings file %s is missing section %s" % (
530 constants._user_settings_,
534 except ConfigParser.NoSectionError, e:
536 "Settings file %s is missing section %s" % (
537 constants._user_settings_,
542 def save_settings(self, config):
544 @note Thread Agnostic
546 config.add_section(constants.__pretty_app_name__)
547 config.set(constants.__pretty_app_name__, "active", str(self._selectedBackendId))
548 config.set(constants.__pretty_app_name__, "orientation", str(int(gtk_toolbox.get_screen_orientation())))
549 for i, value in enumerate(self._credentials):
550 blob = base64.b64encode(value)
551 config.set(constants.__pretty_app_name__, "bin_blob_%i" % i, blob)
552 config.add_section("alarm")
553 if self._alarmHandler is not None:
554 self._alarmHandler.save_settings(config, "alarm")
556 for backendId, view in itertools.chain(
557 self._dialpads.iteritems(),
558 self._accountViews.iteritems(),
559 self._messagesViews.iteritems(),
560 self._recentViews.iteritems(),
561 self._contactsViews.iteritems(),
563 sectionName = "%s - %s" % (backendId, view.name())
564 config.add_section(sectionName)
565 view.save_settings(config, sectionName)
567 def _save_settings(self):
569 @note Thread Agnostic
571 config = ConfigParser.SafeConfigParser()
572 self.save_settings(config)
573 with open(constants._user_settings_, "wb") as configFile:
574 config.write(configFile)
576 def _refresh_active_tab(self):
577 pageIndex = self._notebook.get_current_page()
578 if pageIndex == self.CONTACTS_TAB:
579 self._contactsViews[self._selectedBackendId].update(force=True)
580 elif pageIndex == self.RECENT_TAB:
581 self._recentViews[self._selectedBackendId].update(force=True)
582 elif pageIndex == self.MESSAGES_TAB:
583 self._messagesViews[self._selectedBackendId].update(force=True)
585 if pageIndex in (self.RECENT_TAB, self.MESSAGES_TAB):
586 if self._ledHandler is not None:
587 self._ledHandler.off()
589 def _on_close(self, *args, **kwds):
591 if self._osso is not None:
595 self._save_settings()
599 def _on_device_state_change(self, shutdown, save_unsaved_data, memory_low, system_inactivity, message, userData):
601 For shutdown or save_unsaved_data, our only state is cookies and I think the cookie manager handles that for us.
602 For system_inactivity, we have no background tasks to pause
604 @note Hildon specific
608 for backendId in self.BACKENDS:
609 self._phoneBackends[backendId].clear_caches()
610 self._contactsViews[self._selectedBackendId].clear_caches()
613 if save_unsaved_data or shutdown:
614 self._save_settings()
616 self._errorDisplay.push_exception()
618 def _on_connection_change(self, connection, event, magicIdentifier):
620 @note Hildon specific
625 status = event.get_status()
626 error = event.get_error()
627 iap_id = event.get_iap_id()
628 bearer = event.get_bearer_type()
630 if status == conic.STATUS_CONNECTED:
632 self._spawn_attempt_login(2)
633 elif status == conic.STATUS_DISCONNECTED:
635 self._defaultBackendId = self._selectedBackendId
636 self._change_loggedin_status(self.NULL_BACKEND)
638 self._errorDisplay.push_exception()
640 def _on_window_state_change(self, widget, event, *args):
642 @note Hildon specific
645 if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
646 self._isFullScreen = True
648 self._isFullScreen = False
650 self._errorDisplay.push_exception()
652 def _on_key_press(self, widget, event, *args):
654 @note Hildon specific
657 if event.keyval == gtk.keysyms.F6:
658 if self._isFullScreen:
659 self._window.unfullscreen()
661 self._window.fullscreen()
663 self._errorDisplay.push_exception()
665 def _on_clearcookies_clicked(self, *args):
667 self._phoneBackends[self._selectedBackendId].logout()
668 self._accountViews[self._selectedBackendId].clear()
669 self._recentViews[self._selectedBackendId].clear()
670 self._messagesViews[self._selectedBackendId].clear()
671 self._contactsViews[self._selectedBackendId].clear()
672 self._change_loggedin_status(self.NULL_BACKEND)
674 self._spawn_attempt_login(2, True)
676 self._errorDisplay.push_exception()
678 def _on_notebook_switch_page(self, notebook, page, pageIndex):
680 self._reset_tab_refresh()
682 didRecentUpdate = False
683 didMessagesUpdate = False
685 if pageIndex == self.RECENT_TAB:
686 didRecentUpdate = self._recentViews[self._selectedBackendId].update()
687 elif pageIndex == self.MESSAGES_TAB:
688 didMessagesUpdate = self._messagesViews[self._selectedBackendId].update()
689 elif pageIndex == self.CONTACTS_TAB:
690 self._contactsViews[self._selectedBackendId].update()
691 elif pageIndex == self.ACCOUNT_TAB:
692 self._accountViews[self._selectedBackendId].update()
694 if didRecentUpdate or didMessagesUpdate:
695 if self._ledHandler is not None:
696 self._ledHandler.off()
698 self._errorDisplay.push_exception()
700 def _set_tab_refresh(self, *args):
702 pageIndex = self._notebook.get_current_page()
703 child = self._notebook.get_nth_page(pageIndex)
704 self._notebook.get_tab_label(child).set_text("Refresh?")
706 self._errorDisplay.push_exception()
709 def _reset_tab_refresh(self, *args):
711 pageIndex = self._notebook.get_current_page()
712 child = self._notebook.get_nth_page(pageIndex)
713 self._notebook.get_tab_label(child).set_text(self._originalCurrentLabels[pageIndex])
715 self._errorDisplay.push_exception()
718 def _on_tab_refresh(self, *args):
720 self._refresh_active_tab()
721 self._reset_tab_refresh()
723 self._errorDisplay.push_exception()
726 def _on_sms_clicked(self, number, message):
728 assert number, "No number specified"
729 assert message, "Empty message"
731 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
734 self._errorDisplay.push_exception()
738 self._errorDisplay.push_message(
739 "Backend link with GoogleVoice is not working, please try again"
745 self._phoneBackends[self._selectedBackendId].send_sms(number, message)
746 hildonize.show_information_banner(self._window, "Sending to %s" % number)
749 self._errorDisplay.push_exception()
752 self._dialpads[self._selectedBackendId].clear()
754 self._errorDisplay.push_exception()
756 def _on_dial_clicked(self, number):
758 assert number, "No number to call"
760 loggedIn = self._phoneBackends[self._selectedBackendId].is_authed()
763 self._errorDisplay.push_exception()
767 self._errorDisplay.push_message(
768 "Backend link with GoogleVoice is not working, please try again"
774 assert self._phoneBackends[self._selectedBackendId].get_callback_number() != "", "No callback number specified"
775 self._phoneBackends[self._selectedBackendId].dial(number)
776 hildonize.show_information_banner(self._window, "Calling %s" % number)
779 self._errorDisplay.push_exception()
782 self._dialpads[self._selectedBackendId].clear()
784 self._errorDisplay.push_exception()
786 def _on_menu_refresh(self, *args):
788 self._refresh_active_tab()
790 self._errorDisplay.push_exception()
792 def _on_paste(self, *args):
794 contents = self._clipboard.wait_for_text()
795 if contents is not None:
796 self._dialpads[self._selectedBackendId].set_number(contents)
798 self._errorDisplay.push_exception()
800 def _on_about_activate(self, *args):
802 dlg = gtk.AboutDialog()
803 dlg.set_name(constants.__pretty_app_name__)
804 dlg.set_version("%s-%d" % (constants.__version__, constants.__build__))
805 dlg.set_copyright("Copyright 2008 - LGPL")
806 dlg.set_comments("Dialcentral is a touch screen enhanced interface to your GoogleVoice account. This application is not affiliated with Google in any way")
807 dlg.set_website("http://gc-dialer.garage.maemo.org/")
808 dlg.set_authors(["<z2n@merctech.com>", "Eric Warnke <ericew@gmail.com>", "Ed Page <edpage@byu.net>"])
812 self._errorDisplay.push_exception()
818 failureCount, testCount = doctest.testmod()
820 print "Tests Successful"
827 _lock_file = os.path.join(constants._data_path_, ".lock")
828 #with gtk_toolbox.flock(_lock_file, 0):
829 gtk.gdk.threads_init()
831 if hildonize.IS_HILDON_SUPPORTED:
832 gtk.set_application_name(constants.__pretty_app_name__)
833 handle = Dialcentral()
837 class DummyOptions(object):
843 if __name__ == "__main__":
844 logging.basicConfig(level=logging.DEBUG)
846 if len(sys.argv) > 1:
852 if optparse is not None:
853 parser = optparse.OptionParser()
854 parser.add_option("-t", "--test", action="store_true", dest="test", help="Run tests")
855 (commandOptions, commandArgs) = parser.parse_args()
857 commandOptions = DummyOptions()
860 if commandOptions.test: