Merge lp:~jameinel/bzr-explorer/wt_model into lp:~bzr-explorer-dev/bzr-explorer/trunk.old

Proposed by John A Meinel on 2009-07-09
Status: Rejected
Rejected by: Alexander Belchenko on 2010-02-20
Proposed branch: lp:~jameinel/bzr-explorer/wt_model
Merge into: lp:~bzr-explorer-dev/bzr-explorer/trunk.old
Diff against target: None lines
To merge this branch: bzr merge lp:~jameinel/bzr-explorer/wt_model
Reviewer Review Type Date Requested Status
Bazaar Explorer Developers 2009-07-09 Pending
Review via email: mp+8488@code.launchpad.net
To post a comment you must log in.
John A Meinel (jameinel) wrote :

I don't think this is 100% ready for merging, but I figured I should make sure other people were aware that I was working on it.

This patch builds on my earlier "add a test suite" patches, so hopefully that noise doesn't bother anyone (since those still haven't landed yet).

Anyway, this add as WorkingTreeModel class. It could be arguably called an "IterChangesModel" because it wraps the result of iter_changes() into a QtAbstractItemModel class.

That basically allows you to see the versioned status, etc. It uses want_unversioned=True so that it gives the status of all files (if only because you need a directory to be able to put children underneath it.)

Some of the motivation was wanting to be able to handle:
https://bugs.edge.launchpad.net/bzr-explorer/+bug/396662
(working tree should de-emphasize ignored files)

I was also wondering if something like this would be better placed into QBzr, since they likely need it there as well.

Alexander Belchenko (bialix) wrote :

I think the current approach is to use treewidget from qbzr. I think this won't be merged.

John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Alexander Belchenko wrote:
> I think the current approach is to use treewidget from qbzr. I think this won't be merged.

Feel free to reject it, I'm only getting an OOPS when I try to visit the
LP page.

John
=:->

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkuC2agACgkQJdeBCYSNAAMNmwCgix6ye3NGh1ptktgRvO8vHKhk
d3YAnAxuk7IV9rdrpedj901I2UvuZz4N
=IvWj
-----END PGP SIGNATURE-----

Unmerged revisions

181. By John A Meinel on 2009-07-09

We now pick a different bunch of columns.

It actually looks pretty interesting now.

180. By John A Meinel on 2009-07-09

Include the full set of values that are given from iter_changes.

179. By John A Meinel on 2009-07-09

Switch to using WT.iter_changes(WT.basis_tree())

This gives us more status information, which makes it more interesting
overall.

178. By John A Meinel on 2009-07-09

Add a decoration for column 0 from the QDirModel

177. By John A Meinel on 2009-07-09

Merge basic-tests updates

176. By John A Meinel on 2009-07-08

Start trying to add pixmaps for the WTModel.

175. By John A Meinel on 2009-07-07

Fix a couple edge cases in the wt_browser.py that were causing issues

174. By John A Meinel on 2009-07-07

Update the WorkingTreeModel to have a genuinely artificial root item.

173. By John A Meinel on 2009-07-07

Hack in the new model to get it viewable.
It would seem I need to re-think the root object.
It seems views have to have an indexable object to view.
Which means that we need an arbitrary root object, and then
the root-id of the filesystem will be a child of that.

172. By John A Meinel on 2009-07-07

Implement support for WTModel.data(), I believe this makes a complete model.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '__init__.py'
2--- __init__.py 2009-06-30 10:55:21 +0000
3+++ __init__.py 2009-07-07 19:39:48 +0000
4@@ -29,7 +29,7 @@
5 lazy_import(globals(), '''
6 from PyQt4 import QtGui, QtCore
7
8-from bzrlib.plugins.explorer.lib.explorer import QExplorerMainWindow
9+from bzrlib.plugins.explorer.lib import explorer
10 ''')
11
12
13@@ -38,15 +38,9 @@
14 import bzrlib.plugins.qbzr.lib
15
16
17-def test_suite():
18- """Called by bzrlib to fetch tests for this plugin"""
19- from unittest import TestSuite, TestLoader
20- #from bzrlib.plugins.explorer.lib.tests import ...
21- loader = TestLoader()
22- suite = TestSuite()
23- for module in []:
24- suite.addTests(loader.loadTestsFromModule(module))
25- return suite
26+def load_tests(basic_tests, module, loader):
27+ from bzrlib.plugins.explorer.tests import load_tests
28+ return load_tests(basic_tests, module, loader)
29
30
31 class cmd_explorer(Command):
32@@ -114,7 +108,7 @@
33 # TODO: setup exception handler ala QBzrCommand
34 # (inheriting directly from QBzrCommands cause a metadata error?)
35 app = QtGui.QApplication(sys.argv)
36- window = QExplorerMainWindow(location=location, profile=hat,
37+ window = explorer.QExplorerMainWindow(location=location, profile=hat,
38 desktop=desktop, dry_run=dry_run, preferences=prefs)
39 window.show()
40 app.exec_()
41
42=== modified file 'lib/wt_browser.py'
43--- lib/wt_browser.py 2009-07-02 06:50:18 +0000
44+++ lib/wt_browser.py 2009-07-07 22:37:42 +0000
45@@ -19,6 +19,9 @@
46 from PyQt4 import QtCore, QtGui
47
48 from bzrlib.plugins.explorer.lib.i18n import gettext, N_
49+from bzrlib.plugins.explorer.lib import (
50+ wt_model,
51+ )
52 from bzrlib.plugins.explorer.lib.kinds import (
53 OPEN_FOLDER_ACTION,
54 EDIT_ACTION,
55@@ -39,21 +42,22 @@
56
57 def _build_ui(self):
58 # Init them model and view
59- model = QtGui.QDirModel()
60+ from bzrlib import workingtree
61+ model = wt_model.WorkingTreeModel(workingtree.WorkingTree.open('.'))
62 # sort flags for tree view: direcotries first,
63 # on Windows items are case-insensitive
64 flags = QtCore.QDir.SortFlags(QtCore.QDir.Name|QtCore.QDir.DirsFirst)
65 if sys.platform == 'win32':
66 flags |= QtCore.QDir.IgnoreCase
67- model.setSorting(flags)
68+ # model.setSorting(flags)
69 view = QtGui.QTreeView()
70 view.setModel(model)
71
72 # Hide all except the Name column
73- for i in range(1, model.columnCount()):
74- view.setColumnHidden(i, True)
75- if getattr(view, 'setHeaderHidden', None):
76- view.setHeaderHidden(True)
77+ # for i in range(1, model.columnCount()):
78+ # view.setColumnHidden(i, True)
79+ # if getattr(view, 'setHeaderHidden', None):
80+ # view.setHeaderHidden(True)
81
82 # Build a button bar for the view
83 browse_button = QtGui.QPushButton()
84@@ -131,6 +135,7 @@
85 self._action_callback("open", unicode(path))
86
87 def do_current_changed(self, model_index, prev_model_index):
88+ return
89 model = model_index.model()
90 fileinfo = model.fileInfo(model_index)
91 self._edit_button.setEnabled(fileinfo and not fileinfo.isDir())
92@@ -147,7 +152,7 @@
93 else:
94 # Show the tree
95 self.stacked.setCurrentIndex(1)
96- index = self.model.index(path)
97+ index = self.model.index(0, 0, None)
98 self.view.setRootIndex(index)
99 self._root = path
100 self._selected_fileinfo = None
101
102=== added file 'lib/wt_model.py'
103--- lib/wt_model.py 1970-01-01 00:00:00 +0000
104+++ lib/wt_model.py 2009-07-09 19:46:58 +0000
105@@ -0,0 +1,269 @@
106+# Copyright (C) 2009 Canonical Ltd
107+#
108+# This program is free software; you can redistribute it and/or modify
109+# it under the terms of the GNU General Public License as published by
110+# the Free Software Foundation; either version 2 of the License, or
111+# (at your option) any later version.
112+#
113+# This program is distributed in the hope that it will be useful,
114+# but WITHOUT ANY WARRANTY; without even the implied warranty of
115+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
116+# GNU General Public License for more details.
117+#
118+# You should have received a copy of the GNU General Public License
119+# along with this program; if not, write to the Free Software
120+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
121+
122+"""Implement a Model defining interactions with a WorkingTree"""
123+
124+from bzrlib import osutils, workingtree
125+
126+from PyQt4 import QtCore, QtGui
127+
128+
129+# TODO: consider that this is similar to what one would want for an
130+# InventoryModel, except we should have extra attributes like
131+# "is_modified", etc. Also consider that for a working tree, we probably
132+# need to handle files that are unknown and ignored.
133+class _WTItem(object):
134+ """Encapsulates the display information for a given item.
135+
136+ :ivar children:
137+ :type children: A list of _WTItem objects
138+ """
139+
140+ _versioned_map = {(True, True): ' ', # versioned in both
141+ (True, False): '-', # versioned in source
142+ (False, True): '+', # versioned in target
143+ (False, False): '-', # versioned in neither
144+ }
145+
146+ def __init__(self, file_id, is_modified, path, name, kind, versioned,
147+ executable, parent_id, old_path, old_name, old_kind,
148+ was_versioned, old_executable, old_parent_id, parent,
149+ ignore_pattern):
150+ """Create a new _WTItem
151+ """
152+ self.file_id = file_id
153+ self.path = path
154+ self.name = name
155+ self.kind = kind
156+ self.versioned = versioned
157+ self.is_modified = is_modified
158+ self.ignore_pattern = ignore_pattern
159+ self.executable = executable
160+ self.parent_id = parent_id
161+ self.old_path = old_path
162+ self.old_kind = old_kind
163+ self.old_name = old_name
164+ self.was_versioned = was_versioned
165+ self.old_executable = old_executable
166+ self.old_parent_id = old_parent_id
167+ self.parent = parent
168+ # Note: this creates a cyclical reference, consider using parent
169+ # instead
170+ self.children = []
171+ # Determine the status of this file
172+ self.status = None
173+ if self.versioned is not None and self.was_versioned is not None:
174+ self._determine_status()
175+
176+ def _determine_status(self):
177+ if self.ignore_pattern is not None:
178+ self.status = 'I '
179+ return
180+ # Generally copied from bzrlib.delta.report_changes
181+ exe_status = ' '
182+ # files are "renamed" if they are moved or if name changes, as long
183+ # as it had a value
184+ if (self.old_name is not None and self.name is not None
185+ and self.old_parent_id is not None
186+ and self.parent_id is not None
187+ and (self.old_name != self.name
188+ or self.parent_id != self.old_parent_id)):
189+ versioned_status = 'R' # Renamed implies versioned in both
190+ else:
191+ versioned_status = self._versioned_map[
192+ (self.was_versioned, self.versioned)]
193+ if self.old_kind != self.kind:
194+ if self.old_kind is None:
195+ modified_status = 'N' # created
196+ elif self.kind is None:
197+ modified_status = 'D' # deleted
198+ else:
199+ modified_status = 'K' # kind changed
200+ else:
201+ if self.is_modified:
202+ modified_status = 'M' # modified
203+ else:
204+ modified_status = ' ' # unchanged
205+ if self.kind == 'file':
206+ if (self.executable != self.old_executable):
207+ exe_status = '*'
208+ self.status = modified_status + versioned_status + exe_status
209+
210+ def __repr__(self):
211+ return '%s(%s)' % (self.__class__.__name__,
212+ self.__dict__)
213+
214+ def row(self):
215+ """Return the row for this object in the parents children list."""
216+ if self.parent is None:
217+ return 0
218+ return self.parent.children.index(self)
219+
220+ def _from_iter_changes(self, wt, changes):
221+ dir_by_id = {}
222+ dir_by_path = {}
223+ for (file_id, paths, did_change, versions, parent_ids, names, kinds,
224+ executables) in changes:
225+ if file_id is None:
226+ # not versioned
227+ parent_dir = osutils.dirname(paths[1])
228+ if names[1] == '': # unversioned root?
229+ raise ValueError('not supported yet')
230+ parent = dir_by_path[parent_dir]
231+ ignore_pattern = wt.is_ignored(paths[1])
232+ else:
233+ if parent_ids[1] is None: # Root entry
234+ # Or maybe this happens for unversioned files?
235+ assert names[1] == ''
236+ parent = self
237+ else:
238+ parent = dir_by_id[parent_ids[1]]
239+ ignore_pattern = None
240+ item = _WTItem(file_id, did_change, paths[1], names[1], kinds[1],
241+ versions[1], executables[1], parent_ids[1],
242+ paths[0], names[0], kinds[0], versions[0],
243+ executables[0], parent_ids[0], parent,
244+ ignore_pattern)
245+ parent.children.append(item)
246+ if kinds[1] == 'directory':
247+ dir_by_id[file_id] = item
248+ dir_by_path[paths[1]] = item
249+
250+ @classmethod
251+ def create_from_wt(cls, wt):
252+ """Create the _WTItem tree structure from an inventory."""
253+ wt.lock_read()
254+ try:
255+ basis_tree = wt.basis_tree()
256+ basis_tree.lock_read()
257+ try:
258+ root_wt_item = cls(*([None]*16))
259+ changes = wt.iter_changes(basis_tree, include_unchanged=True,
260+ require_versioned=False, want_unversioned=True)
261+ root_wt_item._from_iter_changes(wt, changes)
262+ finally:
263+ basis_tree.unlock()
264+ finally:
265+ wt.unlock()
266+ return root_wt_item
267+
268+
269+class WorkingTreeModel(QtCore.QAbstractItemModel):
270+
271+ _headers = [
272+ 'name',
273+ 'kind',
274+ 'status',
275+ 'path',
276+ ]
277+ _column_to_attribute = dict(enumerate(_headers))
278+
279+ def __init__(self, wt, parent=None):
280+ QtCore.QAbstractItemModel.__init__(self, parent)
281+ self._wt = wt
282+ self._dirmodel = QtGui.QDirModel()
283+ self._root_wt_item = _WTItem.create_from_wt(self._wt)
284+
285+ def index(self, row, column, parent=None):
286+ """Return the QModelIndex for the given item.
287+
288+ :param row: In this case, the Nth file for the given directory
289+ :type row: int
290+ :param column: The data attribute for the given file/dir
291+ :type column: int
292+ :param parent: the containing directory
293+ :type parent: QModelIndex
294+ :return: The index object to reference the given data
295+ :rtype: QModelIndex
296+ """
297+ if parent is None:
298+ parent = QtCore.QModelIndex()
299+ parent_item = self._root_wt_item
300+ if not self.hasIndex(row, column, parent):
301+ return QtCore.QModelIndex()
302+
303+ if not parent.isValid():
304+ parent_item = self._root_wt_item
305+ else:
306+ parent_item = parent.internalPointer()
307+ child = parent_item.children[row]
308+ return self.createIndex(row, column, child)
309+
310+ def parent(self, index):
311+ if index is None or not index.isValid():
312+ return QtCore.QModelIndex()
313+
314+ item = index.internalPointer()
315+ if item.parent is None or item.parent is self._root_wt_item:
316+ return QtCore.QModelIndex()
317+ return self.createIndex(item.parent.row(), 0, item.parent)
318+
319+ def rowCount(self, parent=None):
320+ if parent is None or not parent.isValid():
321+ parent_item = self._root_wt_item
322+ else:
323+ parent_item = parent.internalPointer()
324+ return len(parent_item.children)
325+
326+ # implement def hasChildren(self, ...) if rowCount is expensive
327+ # We could do this, by looking at Item.kind
328+ # For now, though, we don't lazily load anything, so leave it alone
329+
330+ def columnCount(self, parent=None):
331+ # this can vary based on parent, but *doesn't*
332+ return len(WorkingTreeModel._headers)
333+
334+ def headerData(self, section, orientation, role):
335+ """Return the header to show for a given entry."""
336+ headers = WorkingTreeModel._headers
337+ if (orientation == QtCore.Qt.Horizontal
338+ and role == QtCore.Qt.DisplayRole
339+ and section < len(headers)):
340+ return QtCore.QVariant(headers[section])
341+ return QtCore.QVariant()
342+
343+ def flags(self, index):
344+ if index is None or not index.isValid():
345+ # This should really be a QtCore.Qt.ItemFlags object, not sure how
346+ # to cast it into one, though
347+ return 0
348+ return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable)
349+
350+ def data(self, index, role):
351+ if index is None or not index.isValid():
352+ return QtCore.QVariant()
353+ # TODO: implement uspport for Qt.DecorationRole, if we return a
354+ # QPixmap, then we will get the displayed icon.
355+ item = index.internalPointer()
356+ if role == QtCore.Qt.DecorationRole and index.column() == 0:
357+ # Return the decoration from an QDirModel
358+ dir_index = self._dirmodel.index(item.path)
359+ data = self._dirmodel.data(dir_index, role)
360+ return data
361+ if role != QtCore.Qt.DisplayRole:
362+ return QtCore.QVariant()
363+ attr = self._column_to_attribute[index.column()]
364+ return QtCore.QVariant(getattr(item, attr, None))
365+
366+ # implement def setData(self, ...) if we want to be editable
367+
368+ # Interesting Role list:
369+ # QtCore.Qt.DisplayRole, QtCore.Qt.ToolTipRole, QtCore.Qt.WhatsThisRole
370+
371+ # Do we want to incrementally populate? Perhaps this would be useful for an
372+ # IterChangesModel
373+ # def fetchMore(self, ...)
374+ # def canFetchmore(self, ...)
375
376=== added directory 'tests'
377=== added file 'tests/__init__.py'
378--- tests/__init__.py 1970-01-01 00:00:00 +0000
379+++ tests/__init__.py 2009-07-09 17:44:53 +0000
380@@ -0,0 +1,88 @@
381+# Copyright (C) 2009 Canonical Ltd
382+#
383+# This program is free software; you can redistribute it and/or modify
384+# it under the terms of the GNU General Public License as published by
385+# the Free Software Foundation; either version 2 of the License, or
386+# (at your option) any later version.
387+#
388+# This program is distributed in the hope that it will be useful,
389+# but WITHOUT ANY WARRANTY; without even the implied warranty of
390+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
391+# GNU General Public License for more details.
392+#
393+# You should have received a copy of the GNU General Public License
394+# along with this program; if not, write to the Free Software
395+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
396+
397+"""Test Suite for bzr-explorer"""
398+
399+from bzrlib.tests import (
400+ TestCase,
401+ TestCaseInTempDir,
402+ TestCaseWithTransport,
403+ )
404+
405+
406+class TestCaseWithQt(TestCaseWithTransport):
407+ """Many GUI functions require a QApplication to be running.
408+
409+ This class ensures a QtApplication is running during setUp.
410+ """
411+
412+ _app = None
413+
414+ def setUp(self):
415+ super(TestCaseWithQt, self).setUp()
416+ self.ensureApp()
417+ self.logger = self.createLogger()
418+
419+ def ensureApp(self):
420+ if TestCaseWithQt._app is None:
421+ from PyQt4 import QtGui
422+ TestCaseWithQt._app = QtGui.QApplication(['bzr-explorer-test-app'])
423+ # It seems we can leave it running.
424+
425+ def createLogger(self, *args):
426+ from PyQt4 import QtCore
427+
428+ class ConnectionLogger(QtCore.QObject):
429+
430+ def __init__(self, *args):
431+ QtCore.QObject.__init__(self, *args)
432+ # A log of actions that have occurrred
433+ self.log = []
434+
435+ def createNamedSlot(self, signal_name):
436+ """Create a new slot, which tracks the signal name given.
437+
438+ :param signal_name: The name of the signal we are connecting
439+ :return: A function that can be used as a signal to this logger.
440+ """
441+ def named_slot(*args):
442+ self.log.append((signal_name,) + args)
443+ return named_slot
444+
445+ def connectToNamedSlot(self, signal, source):
446+ """Create a new named slot, and connect the signal from source.
447+
448+ :param signal: The value to pass to QtCore.SIGNAL, this will
449+ also be the name logged when the signal is activated.
450+ :param source: An object that can generate the given signal.
451+ """
452+ slot = self.createNamedSlot(signal)
453+ QtCore.QObject.connect(source, QtCore.SIGNAL(signal), slot)
454+
455+ return ConnectionLogger(*args)
456+
457+
458+def load_tests(basic_tests, module, loader):
459+ suite = loader.suiteClass()
460+ suite.addTests(basic_tests)
461+
462+ mod_names = [__name__ + '.' + x for x in [
463+ 'test_test_suite',
464+ 'test_welcome',
465+ 'test_wt_model',
466+ ]]
467+ suite.addTests(loader.loadTestsFromModuleNames(mod_names))
468+ return suite
469
470=== added file 'tests/test_test_suite.py'
471--- tests/test_test_suite.py 1970-01-01 00:00:00 +0000
472+++ tests/test_test_suite.py 2009-07-08 20:28:10 +0000
473@@ -0,0 +1,66 @@
474+# Copyright (C) 2009 Canonical Ltd
475+#
476+# This program is free software; you can redistribute it and/or modify
477+# it under the terms of the GNU General Public License as published by
478+# the Free Software Foundation; either version 2 of the License, or
479+# (at your option) any later version.
480+#
481+# This program is distributed in the hope that it will be useful,
482+# but WITHOUT ANY WARRANTY; without even the implied warranty of
483+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
484+# GNU General Public License for more details.
485+#
486+# You should have received a copy of the GNU General Public License
487+# along with this program; if not, write to the Free Software
488+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
489+
490+"""Tests for the testing infrastructure in bzr-explorer."""
491+
492+from PyQt4 import QtCore, QtGui
493+
494+from bzrlib.plugins.explorer import tests
495+
496+
497+class _Emitter(QtCore.QObject):
498+
499+ def trigger_foo(self):
500+ self.emit(QtCore.SIGNAL('foo()'))
501+
502+ def trigger_bar(self, arg1, arg2):
503+ self.emit(QtCore.SIGNAL('bar(int, int)'), arg1, arg2)
504+
505+
506+class TestTestCaseWithQt(tests.TestCaseWithQt):
507+
508+ def test_create_qpixmap(self):
509+ pixmap = QtGui.QPixmap()
510+
511+ def test_logger_named_slot(self):
512+ slot = self.logger.createNamedSlot('trapping_foo')
513+ emitter = _Emitter()
514+ QtCore.QObject.connect(emitter, QtCore.SIGNAL('foo()'), slot)
515+ self.assertEqual([], self.logger.log)
516+ emitter.trigger_foo()
517+ self.assertEqual([('trapping_foo',)], self.logger.log)
518+ emitter.trigger_bar(1, 2) # not connected
519+ self.assertEqual([('trapping_foo',)], self.logger.log)
520+ emitter.trigger_foo()
521+ self.assertEqual([('trapping_foo',), ('trapping_foo',)],
522+ self.logger.log)
523+ slot = self.logger.createNamedSlot('trapping_bar')
524+ QtCore.QObject.connect(emitter, QtCore.SIGNAL('bar(int, int)'), slot)
525+ emitter.trigger_bar(1, 2)
526+ self.assertEqual([('trapping_foo',), ('trapping_foo',),
527+ ('trapping_bar', 1, 2),
528+ ], self.logger.log)
529+
530+ def test_connectToNamedSlot(self):
531+ emitter = _Emitter()
532+ self.logger.connectToNamedSlot('foo()', emitter)
533+ self.logger.connectToNamedSlot('bar(int, int)', emitter)
534+ emitter.trigger_foo()
535+ self.assertEqual([('foo()',)], self.logger.log)
536+ emitter.trigger_bar(3, 4)
537+ self.assertEqual([('foo()',), ('bar(int, int)', 3, 4)],
538+ self.logger.log)
539+
540
541=== added file 'tests/test_welcome.py'
542--- tests/test_welcome.py 1970-01-01 00:00:00 +0000
543+++ tests/test_welcome.py 2009-07-08 19:40:55 +0000
544@@ -0,0 +1,27 @@
545+# Copyright (C) 2009 Canonical Ltd
546+#
547+# This program is free software; you can redistribute it and/or modify
548+# it under the terms of the GNU General Public License as published by
549+# the Free Software Foundation; either version 2 of the License, or
550+# (at your option) any later version.
551+#
552+# This program is distributed in the hope that it will be useful,
553+# but WITHOUT ANY WARRANTY; without even the implied warranty of
554+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
555+# GNU General Public License for more details.
556+#
557+# You should have received a copy of the GNU General Public License
558+# along with this program; if not, write to the Free Software
559+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
560+
561+"""Tests for the WelcomeModel"""
562+
563+from bzrlib.plugins.explorer import tests
564+from bzrlib.plugins.explorer.lib import welcome
565+
566+
567+class TestWelcomeModel(tests.TestCaseInTempDir):
568+
569+ def test__init__(self):
570+ model = welcome.WelcomeModel('a url', None, None, None)
571+
572
573=== added file 'tests/test_wt_model.py'
574--- tests/test_wt_model.py 1970-01-01 00:00:00 +0000
575+++ tests/test_wt_model.py 2009-07-09 19:46:58 +0000
576@@ -0,0 +1,325 @@
577+# Copyright (C) 2009 Canonical Ltd
578+#
579+# This program is free software; you can redistribute it and/or modify
580+# it under the terms of the GNU General Public License as published by
581+# the Free Software Foundation; either version 2 of the License, or
582+# (at your option) any later version.
583+#
584+# This program is distributed in the hope that it will be useful,
585+# but WITHOUT ANY WARRANTY; without even the implied warranty of
586+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
587+# GNU General Public License for more details.
588+#
589+# You should have received a copy of the GNU General Public License
590+# along with this program; if not, write to the Free Software
591+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
592+
593+"""tests for the WorkingTreeModel class"""
594+
595+from PyQt4 import QtCore, QtGui
596+
597+from bzrlib import (
598+ errors,
599+ inventory,
600+ tests,
601+ )
602+
603+from bzrlib.plugins.explorer.lib import wt_model
604+from bzrlib.plugins.explorer.tests import TestCaseWithQt
605+
606+
607+class TestWTModel(TestCaseWithQt):
608+
609+ def test_flags_empty(self):
610+ wt = self.make_branch_and_tree('.')
611+ model = wt_model.WorkingTreeModel(wt)
612+ self.assertEqual(0, model.flags(QtCore.QModelIndex()))
613+
614+ def test_flags_are_readonly(self):
615+ wt = self.make_branch_and_tree('.')
616+ self.build_tree(['a_file'])
617+ model = wt_model.WorkingTreeModel(wt)
618+ index = model.index(0, 0, None)
619+ self.assertEqual(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable,
620+ model.flags(index))
621+
622+ def test_data_invalid(self):
623+ wt = self.make_branch_and_tree('.')
624+ self.build_tree(['a_file'])
625+ model = wt_model.WorkingTreeModel(wt)
626+ res = model.data(QtCore.QModelIndex(), QtCore.Qt.DisplayRole)
627+ self.assertIsInstance(res, QtCore.QVariant)
628+ self.assertFalse(res.isValid())
629+ index = model.index(0, 0, None)
630+ # TODO: We probably could implement ToolTipRole, perhaps returning the
631+ # full path?
632+ res = model.data(QtCore.QModelIndex(), QtCore.Qt.ToolTipRole)
633+ self.assertIsInstance(res, QtCore.QVariant)
634+ self.assertFalse(res.isValid())
635+
636+ def test_data(self):
637+ wt = self.make_branch_and_tree('.')
638+ self.build_tree(['dir/', 'dir/a_file', 'b_file'])
639+ wt.add(['dir', 'dir/a_file', 'b_file'],
640+ ['dir-id', 'a-id', 'b-id'])
641+ model = wt_model.WorkingTreeModel(wt)
642+ root_index = model.index(0, 0, None)
643+ index = model.index(1, 0, root_index)
644+ res = model.data(index, QtCore.Qt.DisplayRole)
645+ self.assertIsInstance(res, QtCore.QVariant)
646+ self.assertEqual('dir', res.toString())
647+ index = model.index(1, 2, root_index)
648+ res = model.data(index, QtCore.Qt.DisplayRole)
649+ self.assertIsInstance(res, QtCore.QVariant)
650+ # New file, just added, no executable
651+ self.assertEqual('N+ ', res.toString())
652+ a_index = model.index(0, 0, index)
653+ res = model.data(a_index, QtCore.Qt.DisplayRole)
654+ self.assertIsInstance(res, QtCore.QVariant)
655+ self.assertEqual('a_file', res.toString())
656+ a_index = model.index(0, 3, index)
657+ res = model.data(a_index, QtCore.Qt.DisplayRole)
658+ self.assertIsInstance(res, QtCore.QVariant)
659+ self.assertEqual('dir/a_file', res.toString())
660+
661+ def test_data_decoraterole(self):
662+ wt = self.make_branch_and_tree('.')
663+ self.build_tree(['a_file.txt'])
664+ wt.add(['a_file.txt'], ['a-id'])
665+ model = wt_model.WorkingTreeModel(wt)
666+ root_index = model.index(0, 0, None)
667+ a_index = model.index(0, 0, root_index)
668+ self.assertEqual('a_file.txt', a_index.internalPointer().name)
669+ res = model.data(a_index, QtCore.Qt.DecorationRole)
670+ self.assertIsInstance(res, QtCore.QVariant)
671+ self.assertTrue(res.isValid())
672+ self.assertEqual('QIcon', res.typeName())
673+ # For records not at column 0, we should get nothing
674+ a_index = model.index(0, 1, root_index)
675+ res = model.data(a_index, QtCore.Qt.DecorationRole)
676+ self.assertIsInstance(res, QtCore.QVariant)
677+ self.assertFalse(res.isValid())
678+
679+ def test_index_invalid(self):
680+ wt = self.make_branch_and_tree('.')
681+ model = wt_model.WorkingTreeModel(wt)
682+ res = model.index(100, 100, None)
683+ self.assertIsInstance(res, QtCore.QModelIndex)
684+ self.assertFalse(res.isValid())
685+
686+ def test_index_empty(self):
687+ wt = self.make_branch_and_tree('.')
688+ model = wt_model.WorkingTreeModel(wt)
689+ res = model.index(0, 0, None)
690+ self.assertIsInstance(res, QtCore.QModelIndex)
691+ self.assertTrue(res.isValid())
692+
693+ def test_index_one_item(self):
694+ wt = self.make_branch_and_tree('.')
695+ self.build_tree(['a_file'])
696+ model = wt_model.WorkingTreeModel(wt)
697+ res = model.index(0, 0, None)
698+ self.assertIsInstance(res, QtCore.QModelIndex)
699+ self.assertTrue(res.isValid())
700+ self.assertEqual('', res.internalPointer().name)
701+
702+ def test_index_subdir(self):
703+ wt = self.make_branch_and_tree('.')
704+ self.build_tree(['dir/', 'dir/a_file', 'b_file'])
705+ wt.add(['dir', 'dir/a_file', 'b_file'],
706+ ['dir-id', 'a-id', 'b-id'])
707+ model = wt_model.WorkingTreeModel(wt)
708+ root_index = model.index(0, 0, None)
709+ self.assertIsInstance(root_index, QtCore.QModelIndex)
710+ self.assertTrue(root_index.isValid())
711+ res = model.index(1, 0, root_index)
712+ self.assertEqual('dir', res.internalPointer().name)
713+ res = model.index(0, 0, res)
714+ self.assertIsInstance(res, QtCore.QModelIndex)
715+ self.assertTrue(res.isValid())
716+ self.assertEqual('a_file', res.internalPointer().name)
717+
718+ def test_rowCount_simple(self):
719+ wt = self.make_branch_and_tree('.')
720+ model = wt_model.WorkingTreeModel(wt)
721+ self.assertEqual(1, model.rowCount())
722+
723+ def test_rowCount_subdirs(self):
724+ wt = self.make_branch_and_tree('.')
725+ self.build_tree(['dir/', 'dir/a_file', 'b_file'])
726+ wt.add(['dir', 'dir/a_file', 'b_file'],
727+ ['dir-id', 'a-id', 'b-id'])
728+ model = wt_model.WorkingTreeModel(wt)
729+ self.assertEqual(1, model.rowCount())
730+ root_index = model.index(0, 0, None)
731+ self.assertIsInstance(root_index, QtCore.QModelIndex)
732+ self.assertTrue(root_index.isValid())
733+ self.assertEqual('', root_index.internalPointer().name)
734+ self.assertEqual(2, model.rowCount(root_index))
735+
736+ def test_parent_empty(self):
737+ wt = self.make_branch_and_tree('.')
738+ model = wt_model.WorkingTreeModel(wt)
739+ res = model.parent(QtCore.QModelIndex())
740+ self.assertIsInstance(res, QtCore.QModelIndex)
741+ self.assertFalse(res.isValid())
742+ res = model.parent(None)
743+ self.assertIsInstance(res, QtCore.QModelIndex)
744+ self.assertFalse(res.isValid())
745+
746+ def test_parent_with_subdirs(self):
747+ wt = self.make_branch_and_tree('.')
748+ self.build_tree(['dir/', 'dir/a_file', 'b_file'])
749+ wt.add(['dir', 'dir/a_file', 'b_file'],
750+ ['dir-id', 'a-id', 'b-id'])
751+ model = wt_model.WorkingTreeModel(wt)
752+ self.assertEqual(1, model.rowCount())
753+ root_index = model.index(0, 0, None)
754+ self.assertIsInstance(root_index, QtCore.QModelIndex)
755+ self.assertTrue(root_index.isValid())
756+ self.assertEqual('', root_index.internalPointer().name)
757+ res = model.parent(root_index)
758+ self.assertIsInstance(res, QtCore.QModelIndex)
759+ self.assertFalse(res.isValid())
760+ dir_index = model.index(1, 0, root_index)
761+ self.assertIsInstance(dir_index, QtCore.QModelIndex)
762+ self.assertTrue(dir_index.isValid())
763+ self.assertEqual('dir', dir_index.internalPointer().name)
764+ res = model.parent(dir_index)
765+ self.assertIsInstance(res, QtCore.QModelIndex)
766+ self.assertTrue(res.isValid())
767+ self.assertEqual('', res.internalPointer().name)
768+
769+ def test_headerData(self):
770+ wt = self.make_branch_and_tree('.')
771+ model = wt_model.WorkingTreeModel(wt)
772+ self.assertEqual(len(model._headers), model.columnCount())
773+ res = model.headerData(0, QtCore.Qt.Horizontal, QtCore.Qt.DisplayRole)
774+ self.assertIsInstance(res, QtCore.QVariant)
775+ self.assertEqual('name', res.toString())
776+ res = model.headerData(1, QtCore.Qt.Horizontal, QtCore.Qt.DisplayRole)
777+ self.assertEqual('kind', res.toString())
778+ res = model.headerData(2, QtCore.Qt.Horizontal, QtCore.Qt.DisplayRole)
779+ self.assertEqual('status', res.toString())
780+ res = model.headerData(3, QtCore.Qt.Horizontal, QtCore.Qt.DisplayRole)
781+ self.assertEqual('path', res.toString())
782+
783+ res = model.headerData(len(model._headers),
784+ QtCore.Qt.Horizontal, QtCore.Qt.DisplayRole)
785+ self.assertIsInstance(res, QtCore.QVariant)
786+ self.assertTrue(res.isNull())
787+ res = model.headerData(0, QtCore.Qt.Vertical, QtCore.Qt.DisplayRole)
788+ self.assertIsInstance(res, QtCore.QVariant)
789+ self.assertTrue(res.isNull())
790+ res = model.headerData(0, QtCore.Qt.Vertical, QtCore.Qt.ToolTipRole)
791+ self.assertIsInstance(res, QtCore.QVariant)
792+ self.assertTrue(res.isNull())
793+
794+
795+class Test_WTItem(tests.TestCaseWithTransport):
796+
797+ def test_from_wt_simple(self):
798+ wt = self.make_branch_and_tree('.')
799+ self.build_tree(['a_file'])
800+ wt.add(['a_file'], ['file-id'])
801+ wt.set_root_id('root-id')
802+ root_item = wt_model._WTItem.create_from_wt(wt)
803+ self.assertIsInstance(root_item, wt_model._WTItem)
804+ self.assertIs(None, root_item.path)
805+ self.assertEqual(1, len(root_item.children))
806+ tree_root_item = root_item.children[0]
807+ self.assertEqual('', tree_root_item.path)
808+ self.assertEqual('', tree_root_item.name)
809+ self.assertEqual('directory', tree_root_item.kind)
810+ self.assertEqual('root-id', tree_root_item.file_id)
811+ self.assertIs(None, tree_root_item.old_kind)
812+ self.assertIs(root_item, tree_root_item.parent)
813+ self.assertEqual(1, len(tree_root_item.children))
814+ child = tree_root_item.children[0]
815+ self.assertEqual('a_file', child.path)
816+ self.assertEqual('a_file', child.name)
817+ self.assertEqual('file', child.kind)
818+ self.assertEqual('file-id', child.file_id)
819+ self.assertIs(None, child.old_kind)
820+ self.assertEqual(tree_root_item, child.parent)
821+ self.assertEqual(0, child.row())
822+
823+ def test_unversioned_from_wt(self):
824+ wt = self.make_branch_and_tree('.')
825+ self.build_tree(['a_file', 'unversioned-file'])
826+ wt.set_root_id('root-id')
827+ wt.add(['a_file'], ['file-id'])
828+ root_item = wt_model._WTItem.create_from_wt(wt)
829+ self.assertIsInstance(root_item, wt_model._WTItem)
830+ self.assertIs(None, root_item.path)
831+ self.assertEqual(1, len(root_item.children))
832+ tree_root_item = root_item.children[0]
833+ self.assertIsInstance(tree_root_item, wt_model._WTItem)
834+ self.assertEqual('', tree_root_item.path)
835+ self.assertEqual('', tree_root_item.name)
836+ self.assertEqual('directory', tree_root_item.kind)
837+ self.assertEqual('root-id', tree_root_item.file_id)
838+ self.assertIs(None, tree_root_item.old_kind)
839+ self.assertIs(root_item, tree_root_item.parent)
840+ self.assertEqual(2, len(tree_root_item.children))
841+ child = tree_root_item.children[0]
842+ self.assertEqual('a_file', child.path)
843+ self.assertEqual('a_file', child.name)
844+ self.assertEqual('file', child.kind)
845+ self.assertEqual('file-id', child.file_id)
846+ self.assertIs(None, child.old_kind)
847+ self.assertEqual(tree_root_item, child.parent)
848+ self.assertEqual(0, child.row())
849+ child = tree_root_item.children[1]
850+ self.assertEqual('unversioned-file', child.path)
851+ self.assertEqual('unversioned-file', child.name)
852+ self.assertEqual('file', child.kind)
853+ self.assertIs(None, child.file_id)
854+ self.assertIs(None, child.old_kind)
855+ self.assertEqual(tree_root_item, child.parent)
856+ self.assertEqual(1, child.row())
857+
858+ def test_with_subdirs(self):
859+ wt = self.make_branch_and_tree('.')
860+ self.build_tree(['dir/', 'dir/subdir/',
861+ 'dir/subdir/a_file'])
862+ wt.set_root_id('root-id')
863+ wt.add(['dir', 'dir/subdir', 'dir/subdir/a_file'],
864+ ['dir-id', 'subdir-id', 'file-id'])
865+ root_item = wt_model._WTItem.create_from_wt(wt)
866+ self.assertIsInstance(root_item, wt_model._WTItem)
867+ self.assertIs(None, root_item.path)
868+ self.assertEqual(1, len(root_item.children))
869+ tree_root_item = root_item.children[0]
870+ self.assertIsInstance(tree_root_item, wt_model._WTItem)
871+ self.assertEqual('root-id', tree_root_item.file_id)
872+ self.assertIs(root_item, tree_root_item.parent)
873+ self.assertEqual(1, len(tree_root_item.children))
874+ child = tree_root_item.children[0]
875+ self.assertEqual('dir', child.path)
876+ self.assertEqual('dir', child.name)
877+ self.assertEqual('directory', child.kind)
878+ self.assertEqual('dir-id', child.file_id)
879+ self.assertIs(None, child.old_kind)
880+ self.assertEqual(tree_root_item, child.parent)
881+ self.assertEqual(0, tree_root_item.row())
882+ self.assertEqual(1, len(child.children))
883+ this_dir = child
884+ child = this_dir.children[0]
885+ self.assertEqual('dir/subdir', child.path)
886+ self.assertEqual('subdir', child.name)
887+ self.assertEqual('directory', child.kind)
888+ self.assertEqual('subdir-id', child.file_id)
889+ self.assertIs(None, child.old_kind)
890+ self.assertEqual(this_dir, child.parent)
891+ self.assertEqual(0, child.row())
892+ self.assertEqual(1, len(child.children))
893+ this_dir = child
894+ child = this_dir.children[0]
895+ self.assertEqual('dir/subdir/a_file', child.path)
896+ self.assertEqual('a_file', child.name)
897+ self.assertEqual('file', child.kind)
898+ self.assertEqual('file-id', child.file_id)
899+ self.assertIs(None, child.old_kind)
900+ self.assertEqual(this_dir, child.parent)
901+ self.assertEqual(0, child.row())

Subscribers

People subscribed via source and target branches