Changeset 204477 in webkit


Ignore:
Timestamp:
Aug 15, 2016, 1:50:13 PM (9 years ago)
Author:
Simon Fraser
Message:

Allow a port to run tests with a custom device setup
https://bugs.webkit.org/show_bug.cgi?id=160833

Reviewed by Daniel Bates.

These changes allow the IOSSimulator port to run tests in iPad mode.

This is made possible by allowing a platform to define CUSTOM_DEVICE_CLASSES,
in this case 'ipad'. When specified, any test in a directory with a suffix that matches
a custom device will be collected into a set, and run in that device's environment after
the other tests have run.

  • Scripts/webkitpy/layout_tests/controllers/manager.py:

(Manager._custom_device_for_test): If the test contains a directory matching a
custom device suffix, return that custom device.
(Manager._set_up_run): Push the custom device class, if any, into options so
that the Worker can get to it.
(Manager.run): Go through the list of tests, and break it down into device-generic
tests, and tests for each device class. _run_test_subset is then called for
each collection of tests, and the results merged.
(Manager._run_test_subset): Some lines unwrapped.
(Manager._end_test_run):
(Manager._run_tests):

  • Scripts/webkitpy/layout_tests/controllers/single_test_runner.py:

(SingleTestRunner.init): Unwrapped a line.

  • Scripts/webkitpy/layout_tests/models/test_run_results.py:

(TestRunResults.merge): Add this function to merge TestRunResults

  • Scripts/webkitpy/layout_tests/views/printing.py:

(Printer.print_workers_and_shards): Print the custom device, if any.

  • Scripts/webkitpy/port/base.py:

(Port): Base port has empty array of custom devices.
(Port.setup_test_run): Add device_class argument.

  • Scripts/webkitpy/port/driver.py:

(DriverInput.repr):
(Driver.check_driver.implementation):

  • Scripts/webkitpy/port/efl.py:

(EflPort.setup_test_run):

  • Scripts/webkitpy/port/gtk.py:

(GtkPort.setup_test_run):

  • Scripts/webkitpy/port/ios.py:

(IOSSimulatorPort): Add CUSTOM_DEVICE_CLASSES for ipad.
(IOSSimulatorPort.init):
(IOSSimulatorPort.simulator_device_type): Use a device name from the DEVICE_CLASS_MAP
based on the custom device class.
(IOSSimulatorPort._set_device_class):
(IOSSimulatorPort._create_simulators): Factor some code into this function.
(IOSSimulatorPort.setup_test_run):
(IOSSimulatorPort.testing_device):
(IOSSimulatorPort.reset_preferences): This used to create the simulator apps, but that
seemed wrong for this function. That was moved to setup_test_run().
(IOSSimulatorPort.check_sys_deps): This function used to create testing devices,
but this happened too early, before we knew which kind of devices to create. Devices
are now created in setup_test_run().

  • Scripts/webkitpy/port/win.py:

(WinPort.setup_test_run):

Location:
trunk/Tools
Files:
13 edited

Legend:

Unmodified
Added
Removed
  • trunk/Tools/ChangeLog

    r204471 r204477  
     12016-08-15  Simon Fraser  <simon.fraser@apple.com>
     2
     3        Allow a port to run tests with a custom device setup
     4        https://bugs.webkit.org/show_bug.cgi?id=160833
     5
     6        Reviewed by Daniel Bates.
     7
     8        These changes allow the IOSSimulator port to run tests in iPad mode.
     9
     10        This is made possible by allowing a platform to define CUSTOM_DEVICE_CLASSES,
     11        in this case 'ipad'. When specified, any test in a directory with a suffix that matches
     12        a custom device will be collected into a set, and run in that device's environment after
     13        the other tests have run.
     14
     15        * Scripts/webkitpy/layout_tests/controllers/manager.py:
     16        (Manager._custom_device_for_test): If the test contains a directory matching a
     17        custom device suffix, return that custom device.
     18        (Manager._set_up_run): Push the custom device class, if any, into options so
     19        that the Worker can get to it.
     20        (Manager.run): Go through the list of tests, and break it down into device-generic
     21        tests, and tests for each device class. _run_test_subset is then called for
     22        each collection of tests, and the results merged.
     23        (Manager._run_test_subset): Some lines unwrapped.
     24        (Manager._end_test_run):
     25        (Manager._run_tests):
     26        * Scripts/webkitpy/layout_tests/controllers/single_test_runner.py:
     27        (SingleTestRunner.__init__): Unwrapped a line.
     28        * Scripts/webkitpy/layout_tests/models/test_run_results.py:
     29        (TestRunResults.merge): Add this function to merge TestRunResults
     30        * Scripts/webkitpy/layout_tests/views/printing.py:
     31        (Printer.print_workers_and_shards): Print the custom device, if any.
     32        * Scripts/webkitpy/port/base.py:
     33        (Port): Base port has empty array of custom devices.
     34        (Port.setup_test_run): Add device_class argument.
     35        * Scripts/webkitpy/port/driver.py:
     36        (DriverInput.__repr__):
     37        (Driver.check_driver.implementation):
     38        * Scripts/webkitpy/port/efl.py:
     39        (EflPort.setup_test_run):
     40        * Scripts/webkitpy/port/gtk.py:
     41        (GtkPort.setup_test_run):
     42        * Scripts/webkitpy/port/ios.py:
     43        (IOSSimulatorPort): Add CUSTOM_DEVICE_CLASSES for ipad.
     44        (IOSSimulatorPort.__init__):
     45        (IOSSimulatorPort.simulator_device_type): Use a device name from the DEVICE_CLASS_MAP
     46        based on the custom device class.
     47        (IOSSimulatorPort._set_device_class):
     48        (IOSSimulatorPort._create_simulators): Factor some code into this function.
     49        (IOSSimulatorPort.setup_test_run):
     50        (IOSSimulatorPort.testing_device):
     51        (IOSSimulatorPort.reset_preferences): This used to create the simulator apps, but that
     52        seemed wrong for this function. That was moved to setup_test_run().
     53        (IOSSimulatorPort.check_sys_deps): This function used to create testing devices,
     54        but this happened too early, before we knew which kind of devices to create. Devices
     55        are now created in setup_test_run().
     56        * Scripts/webkitpy/port/win.py:
     57        (WinPort.setup_test_run):
     58
    1592016-08-15  Daniel Bates  <dabates@apple.com>
    260
  • trunk/Tools/Scripts/webkitpy/layout_tests/controllers/manager.py

    r202362 r204477  
    4040import sys
    4141import time
     42from collections import defaultdict
    4243
    4344from webkitpy.common.checkout.scm.detection import SCMDetector
     
    9899        return self.web_platform_test_subdir in test
    99100
     101    def _custom_device_for_test(self, test):
     102        for device_class in self._port.CUSTOM_DEVICE_CLASSES:
     103            directory_suffix = device_class + self._port.TEST_PATH_SEPARATOR
     104            if directory_suffix in test:
     105                return device_class
     106        return None
     107
    100108    def _http_tests(self, test_names):
    101109        return set(test for test in test_names if self._is_http_test(test))
     
    142150        self._options.child_processes = worker_count
    143151
    144     def _set_up_run(self, test_names):
     152    def _set_up_run(self, test_names, device_class=None):
    145153        self._printer.write_update("Checking build ...")
    146154        if not self._port.check_build(self.needs_servers(test_names)):
    147155            _log.error("Build check failed")
    148156            return False
     157
     158        self._options.device_class = device_class
    149159
    150160        # This must be started before we check the system dependencies,
     
    170180        self._port.host.filesystem.maybe_make_directory(self._results_directory)
    171181
    172         self._port.setup_test_run()
     182        self._port.setup_test_run(self._options.device_class)
    173183        return True
    174184
     
    195205            return test_run_results.RunDetails(exit_code=-1)
    196206
    197         try:
     207        default_device_tests = []
     208
     209        # Look for tests with custom device requirements.
     210        custom_device_tests = defaultdict(list)
     211        for test_file in tests_to_run:
     212            custom_device = self._custom_device_for_test(test_file)
     213            if custom_device:
     214                custom_device_tests[custom_device].append(test_file)
     215            else:
     216                default_device_tests.append(test_file)
     217
     218        if custom_device_tests:
     219            for device_class in custom_device_tests:
     220                _log.debug('{} tests use device {}'.format(len(custom_device_tests[device_class]), device_class))
     221
     222        initial_results = None
     223        retry_results = None
     224        enabled_pixel_tests_in_retry = False
     225
     226        if default_device_tests:
     227            _log.info('')
     228            _log.info("Running %s", pluralize(len(tests_to_run), "test"))
     229            _log.info('')
    198230            if not self._set_up_run(tests_to_run):
    199231                return test_run_results.RunDetails(exit_code=-1)
    200232
     233            initial_results, retry_results, enabled_pixel_tests_in_retry = self._run_test_subset(default_device_tests, tests_to_skip)
     234
     235        for device_class in custom_device_tests:
     236            device_tests = custom_device_tests[device_class]
     237            if device_tests:
     238                _log.info('')
     239                _log.info('Running %s for %s', pluralize(len(device_tests), "test"), device_class)
     240                _log.info('')
     241                if not self._set_up_run(device_tests, device_class):
     242                    return test_run_results.RunDetails(exit_code=-1)
     243
     244                device_initial_results, device_retry_results, device_enabled_pixel_tests_in_retry = self._run_test_subset(device_tests, tests_to_skip)
     245
     246                initial_results = initial_results.merge(device_initial_results) if initial_results else device_initial_results
     247                retry_results = retry_results.merge(device_retry_results) if retry_results else device_retry_results
     248                enabled_pixel_tests_in_retry |= device_enabled_pixel_tests_in_retry
     249
     250        end_time = time.time()
     251        return self._end_test_run(start_time, end_time, initial_results, retry_results, enabled_pixel_tests_in_retry)
     252
     253    def _run_test_subset(self, tests_to_run, tests_to_skip):
     254        try:
    201255            enabled_pixel_tests_in_retry = False
    202             initial_results = self._run_tests(tests_to_run, tests_to_skip, self._options.repeat_each, self._options.iterations,
    203                 int(self._options.child_processes), retrying=False)
     256            initial_results = self._run_tests(tests_to_run, tests_to_skip, self._options.repeat_each, self._options.iterations, int(self._options.child_processes), retrying=False)
    204257
    205258            tests_to_retry = self._tests_to_retry(initial_results, include_crashes=self._port.should_retry_crashes())
     
    212265                _log.info("Retrying %s ..." % pluralize(len(tests_to_retry), "unexpected failure"))
    213266                _log.info('')
    214                 retry_results = self._run_tests(tests_to_retry, tests_to_skip=set(), repeat_each=1, iterations=1,
    215                     num_workers=1, retrying=True)
     267                retry_results = self._run_tests(tests_to_retry, tests_to_skip=set(), repeat_each=1, iterations=1, num_workers=1, retrying=True)
    216268
    217269                if enabled_pixel_tests_in_retry:
     
    222274            self._clean_up_run()
    223275
    224         end_time = time.time()
    225 
     276        return (initial_results, retry_results, enabled_pixel_tests_in_retry)
     277
     278    def _end_test_run(self, start_time, end_time, initial_results, retry_results, enabled_pixel_tests_in_retry):
    226279        # Some crash logs can take a long time to be written out so look
    227280        # for new logs after the test run finishes.
     281
    228282        _log.debug("looking for new crash logs")
    229283        self._look_for_new_crash_logs(initial_results, start_time)
     
    260314
    261315        test_inputs = self._get_test_inputs(tests_to_run, repeat_each, iterations)
     316
    262317        return self._runner.run_tests(self._expectations, test_inputs, tests_to_skip, num_workers, needs_http, needs_websockets, needs_web_platform_test_server, retrying)
    263318
  • trunk/Tools/Scripts/webkitpy/layout_tests/controllers/manager_unittest.py

    r192944 r204477  
    3838from webkitpy.layout_tests.models import test_expectations
    3939from webkitpy.layout_tests.models.test_run_results import TestRunResults
     40from webkitpy.port.test import TestPort
    4041from webkitpy.thirdparty.mock import Mock
    4142from webkitpy.tool.mocktool import MockOptions
     
    100101        manager = get_manager()
    101102        manager._look_for_new_crash_logs(run_results, time.time())
     103
     104    def test_uses_custom_device(self):
     105        class MockCustomDevicePort(TestPort):
     106            CUSTOM_DEVICE_CLASSES = ['starship']           
     107            def __init__(self, host):
     108                super(MockCustomDevicePort, self).__init__(host)
     109
     110        def get_manager():
     111            host = MockHost()
     112            port = MockCustomDevicePort(host)
     113            manager = Manager(port, options=MockOptions(test_list=['fast/test-starship/lasers.html'], http=True), printer=Mock())
     114            return manager
     115
     116        manager = get_manager()
     117        self.assertTrue(manager._custom_device_for_test('fast/test-starship/lasers.html') == 'starship')
     118       
  • trunk/Tools/Scripts/webkitpy/layout_tests/controllers/single_test_runner.py

    r177471 r204477  
    7171                expected_filename = self._port.expected_filename(self._test_name, suffix)
    7272                if self._filesystem.exists(expected_filename):
    73                     _log.error('%s is a reftest, but has an unused expectation file. Please remove %s.',
    74                         self._test_name, expected_filename)
     73                    _log.error('%s is a reftest, but has an unused expectation file. Please remove %s.', self._test_name, expected_filename)
    7574
    7675    def _expected_driver_output(self):
  • trunk/Tools/Scripts/webkitpy/layout_tests/models/test_run_results.py

    r202362 r204477  
    9494            self.slow_tests.add(test_result.test_name)
    9595
     96    def merge(self, test_run_results):
     97        # self.expectations should be the same for both
     98        self.total += test_run_results.total
     99        self.remaining += test_run_results.remaining
     100        self.expected += test_run_results.expected
     101        self.unexpected += test_run_results.unexpected
     102        self.unexpected_failures += test_run_results.unexpected_failures
     103        self.unexpected_crashes += test_run_results.unexpected_crashes
     104        self.unexpected_timeouts += test_run_results.unexpected_timeouts
     105        self.tests_by_expectation.update(test_run_results.tests_by_expectation)
     106        self.tests_by_timeline.update(test_run_results.tests_by_timeline)
     107        self.results_by_name.update(test_run_results.results_by_name)
     108        self.all_results += test_run_results.all_results
     109        self.unexpected_results_by_name.update(test_run_results.unexpected_results_by_name)
     110        self.failures_by_name.update(test_run_results.failures_by_name)
     111        self.total_failures += test_run_results.total_failures
     112        self.expected_skips += test_run_results.expected_skips
     113        self.tests_by_expectation.update(test_run_results.tests_by_expectation)
     114        self.tests_by_timeline.update(test_run_results.tests_by_timeline)
     115        self.slow_tests.update(test_run_results.slow_tests)
     116
     117        self.interrupted |= test_run_results.interrupted
     118        self.keyboard_interrupted |= test_run_results.keyboard_interrupted
     119        return self
    96120
    97121class RunDetails(object):
  • trunk/Tools/Scripts/webkitpy/layout_tests/views/printing.py

    r202819 r204477  
    113113    def print_workers_and_shards(self, num_workers, num_shards):
    114114        driver_name = self._port.driver_name()
     115
     116        device_suffix = ' for device "{}"'.format(self._options.device_class) if self._options.device_class else ''
    115117        if num_workers == 1:
    116             self._print_default("Running 1 %s." % driver_name)
    117             self._print_debug("(%s)." % grammar.pluralize(num_shards, "shard"))
    118         else:
    119             self._print_default("Running %s in parallel." % (grammar.pluralize(num_workers, driver_name)))
    120             self._print_debug("(%d shards)." % num_shards)
     118            self._print_default('Running 1 {}{}.'.format(driver_name, device_suffix))
     119            self._print_debug('({}).'.format(grammar.pluralize(num_shards, "shard")))
     120        else:
     121            self._print_default('Running {} in parallel{}.'.format(grammar.pluralize(num_workers, driver_name), device_suffix))
     122            self._print_debug('({} shards).'.format(num_shards))
    121123        self._print_default('')
    122124
  • trunk/Tools/Scripts/webkitpy/port/base.py

    r204332 r204477  
    8383    DEFAULT_ARCHITECTURE = 'x86'
    8484
     85    CUSTOM_DEVICE_CLASSES = []
     86
    8587    @classmethod
    8688    def determine_full_port_name(cls, host, options, port_name):
     
    804806        return self._build_path('layout-test-results')
    805807
    806     def setup_test_run(self):
     808    def setup_test_run(self, device_class=None):
    807809        """Perform port-specific work at the beginning of a test run."""
    808810        pass
  • trunk/Tools/Scripts/webkitpy/port/driver.py

    r204253 r204477  
    5353        self.args = args or []
    5454
     55    def __repr__(self):
     56        return "DriverInput(test_name='{}', timeout={}, image_hash={}, should_run_pixel_test={}'".format(self.test_name, self.timeout, self.image_hash, self.should_run_pixel_test)
     57
    5558
    5659class DriverOutput(object):
     
    588591
    589592
     593# FIXME: this should be abstracted out via the Port subclass somehow.
    590594class IOSSimulatorDriver(Driver):
    591595    def cmd_line(self, pixel_tests, per_test_args):
  • trunk/Tools/Scripts/webkitpy/port/efl.py

    r203674 r204477  
    5555        return "--efl"
    5656
    57     def setup_test_run(self):
    58         super(EflPort, self).setup_test_run()
     57    def setup_test_run(self, device_class=None):
     58        super(EflPort, self).setup_test_run(device_class)
    5959        self._pulseaudio_sanitizer.unload_pulseaudio_module()
    6060
  • trunk/Tools/Scripts/webkitpy/port/gtk.py

    r203674 r204477  
    101101        return super(GtkPort, self).driver_stop_timeout()
    102102
    103     def setup_test_run(self):
    104         super(GtkPort, self).setup_test_run()
     103    def setup_test_run(self, device_class=None):
     104        super(GtkPort, self).setup_test_run(device_class)
    105105        self._pulseaudio_sanitizer.unload_pulseaudio_module()
    106106
  • trunk/Tools/Scripts/webkitpy/port/ios.py

    r204341 r204477  
    7575    DEFAULT_ARCHITECTURE = 'x86_64'
    7676
     77    DEFAULT_DEVICE_CLASS = 'iphone'
     78    CUSTOM_DEVICE_CLASSES = ['ipad']
     79
    7780    SIMULATOR_BUNDLE_ID = 'com.apple.iphonesimulator'
    7881    relay_name = 'LayoutTestRelay'
     
    8184    PROCESS_COUNT_ESTIMATE_PER_SIMULATOR_INSTANCE = 100
    8285
     86    DEVICE_CLASS_MAP = {
     87        'x86_64': {
     88            'iphone': 'iPhone 5s',
     89            'ipad': 'iPad Air'
     90        },
     91        'x86': {
     92            'iphone': 'iPhone 5',
     93            'ipad': 'iPad Retina'
     94        },
     95    }
     96
    8397    def __init__(self, host, port_name, **kwargs):
    8498        super(IOSSimulatorPort, self).__init__(host, port_name, **kwargs)
     99
     100        optional_device_class = self.get_option('device_class')
     101        self._device_class = optional_device_class if optional_device_class else self.DEFAULT_DEVICE_CLASS
     102        _log.debug('IOSSimulatorPort _device_class is %s', self._device_class)
    85103
    86104    def driver_name(self):
     
    101119        return runtime
    102120
    103     @property
    104     @memoized
    105121    def simulator_device_type(self):
    106122        device_type_identifier = self.get_option('device_type')
    107123        if device_type_identifier:
     124            _log.debug('simulator_device_type for device identifier %s', device_type_identifier)
    108125            device_type = DeviceType.from_identifier(device_type_identifier)
    109126        else:
    110             if self.architecture() == 'x86_64':
    111                 device_type = DeviceType.from_name('iPhone 5s')
    112             else:
    113                 device_type = DeviceType.from_name('iPhone 5')
     127            _log.debug('simulator_device_type for device %s', self._device_class)
     128            device_name = self.DEVICE_CLASS_MAP[self.architecture()][self._device_class]
     129            if not device_name:
     130                raise Exception('Failed to find device for architecture {} and device class {}'.format(self.architecture()), self._device_class)
     131            device_type = DeviceType.from_name(device_name)
    114132        return device_type
    115133
     
    206224        return list(reversed([self._filesystem.join(self._webkit_baseline_path(p), 'TestExpectations') for p in self.baseline_search_path()]))
    207225
    208     def setup_test_run(self):
     226    def _set_device_class(self, device_class):
     227        # Ideally we'd ensure that no simulators are running when this is called.
     228        self._device_class = device_class if device_class else self.DEFAULT_DEVICE_CLASS
     229
     230    def _create_simulators(self):
     231        if (self.default_child_processes() < self.child_processes()):
     232                _log.warn("You have specified very high value({0}) for --child-processes".format(self.child_processes()))
     233                _log.warn("maximum child-processes which can be supported on this system are: {0}".format(self.default_child_processes()))
     234                _log.warn("This is very likely to fail.")
     235
     236        self._createSimulatorApps()
     237
     238        for i in xrange(self.child_processes()):
     239            Simulator.wait_until_device_is_in_state(self.testing_device(i).udid, Simulator.DeviceState.SHUTDOWN)
     240            Simulator.reset_device(self.testing_device(i).udid)
     241
     242    def setup_test_run(self, device_class=None):
    209243        mac_os_version = self.host.platform.os_version
     244
     245        self._set_device_class(device_class)
     246
     247        _log.debug('')
     248        _log.debug('setup_test_run for %s', self._device_class)
     249
     250        self._create_simulators()
     251
    210252        for i in xrange(self.child_processes()):
    211253            device_udid = self.testing_device(i).udid
     254            _log.debug('testing device %s has udid %s', i, device_udid)
     255
    212256            # FIXME: <rdar://problem/20916140> Switch to using CoreSimulator.framework for launching and quitting iOS Simulator
    213 
    214257            self._executive.run_command([
    215258                'open', '-g', '-b', self.SIMULATOR_BUNDLE_ID + str(i),
     
    285328            _log.error('The iOS Simulator runtime with identifier "{0}" cannot be used because it is unavailable.'.format(self.simulator_runtime.identifier))
    286329            return False
    287         for i in xrange(self.child_processes()):
    288             # FIXME: This creates the devices sequentially, doing this in parallel can improve performance.
    289             testing_device = self.testing_device(i)
    290330        return super(IOSSimulatorPort, self).check_sys_deps(needs_http)
    291331
     
    330370        return stderr, crash_log
    331371
    332     @memoized
    333372    def testing_device(self, number):
    334         return Simulator().lookup_or_create_device(self.simulator_device_type.name + ' WebKit Tester' + str(number), self.simulator_device_type, self.simulator_runtime)
     373        # FIXME: rather than calling lookup_or_create_device every time, we should just store a mapping of
     374        # number to device_udid.
     375        device_type = self.simulator_device_type()
     376        _log.debug(' testing_device %s using device_type %s', number, device_type)
     377        return Simulator().lookup_or_create_device(device_type.name + ' WebKit Tester' + str(number), device_type, self.simulator_runtime)
    335378
    336379    def get_simulator_path(self, suffix=""):
     
    351394    def reset_preferences(self):
    352395        _log.debug("reset_preferences")
    353         if (self.default_child_processes() < self.child_processes()):
    354                 _log.warn("You have specified very high value({0}) for --child-processes".format(self.child_processes()))
    355                 _log.warn("maximum child-processes which can be supported on this system are: {0}".format(self.default_child_processes()))
    356                 _log.warn("This is very likely to fail.")
    357 
    358396        self._quit_ios_simulator()
    359         self._createSimulatorApps()
    360 
    361         for i in xrange(self.child_processes()):
    362             Simulator.wait_until_device_is_in_state(self.testing_device(i).udid, Simulator.DeviceState.SHUTDOWN)
    363             Simulator.reset_device(self.testing_device(i).udid)
     397        # Maybe this should delete all devices that we've created?
    364398
    365399    def nm_command(self):
  • trunk/Tools/Scripts/webkitpy/port/test.py

    r202362 r204477  
    458458        return '/tmp/layout-test-results'
    459459
    460     def setup_test_run(self):
     460    def setup_test_run(self, device_class=None):
    461461        pass
    462462
  • trunk/Tools/Scripts/webkitpy/port/win.py

    r204271 r204477  
    346346        os.system("rm -rf /dev/shm/sem.*")
    347347
    348     def setup_test_run(self):
     348    def setup_test_run(self, device_class=None):
    349349        atexit.register(self.restore_crash_log_saving)
    350350        self.setup_crash_log_saving()
    351351        self.prevent_error_dialogs()
    352352        self.delete_sem_locks()
    353         super(WinPort, self).setup_test_run()
     353        super(WinPort, self).setup_test_run(device_class)
    354354
    355355    def clean_up_test_run(self):
Note: See TracChangeset for help on using the changeset viewer.