[Notes] [Git][BuildStream/buildstream][danielsilverstone-ct/further-optimisations] 17 commits: _includes: better provenance on recursive include



Title: GitLab

Daniel Silverstone pushed to branch danielsilverstone-ct/further-optimisations at BuildStream / buildstream

Commits:

8 changed files:

Changes:

  • .gitlab-ci.yml
    ... ... @@ -166,6 +166,7 @@ tests-wsl:
    166 166
     
    
    167 167
       script:
    
    168 168
       - "${TEST_COMMAND}"
    
    169
    +  when: manual
    
    169 170
     
    
    170 171
     # Automatically build documentation for every commit, we want to know
    
    171 172
     # if building documentation fails even if we're not deploying it.
    

  • buildstream/_cachekey.py
    ... ... @@ -40,3 +40,20 @@ def generate_key(value):
    40 40
         ordered = _yaml.node_sanitize(value)
    
    41 41
         string = pickle.dumps(ordered)
    
    42 42
         return hashlib.sha256(string).hexdigest()
    
    43
    +
    
    44
    +
    
    45
    +# generate_key_pre_sanitized()
    
    46
    +#
    
    47
    +# Generate an sha256 hex digest from the given value. The value
    
    48
    +# must be (a) compatible with generate_key() and (b) already have
    
    49
    +# been passed through _yaml.node_sanitize()
    
    50
    +#
    
    51
    +# Args:
    
    52
    +#    value: A sanitized value to get a key for
    
    53
    +#
    
    54
    +# Returns:
    
    55
    +#    (str): An sha256 hex digest of the given value
    
    56
    +#
    
    57
    +def generate_key_pre_sanitized(value):
    
    58
    +    string = pickle.dumps(value)
    
    59
    +    return hashlib.sha256(string).hexdigest()

  • buildstream/_cas/cascache.py
    ... ... @@ -35,6 +35,8 @@ from .._exceptions import CASCacheError
    35 35
     
    
    36 36
     from .casremote import BlobNotFound, _CASBatchRead, _CASBatchUpdate
    
    37 37
     
    
    38
    +_BUFFER_SIZE = 65536
    
    39
    +
    
    38 40
     
    
    39 41
     # A CASCache manages a CAS repository as specified in the Remote Execution API.
    
    40 42
     #
    
    ... ... @@ -371,7 +373,7 @@ class CASCache():
    371 373
                 with contextlib.ExitStack() as stack:
    
    372 374
                     if path is not None and link_directly:
    
    373 375
                         tmp = stack.enter_context(open(path, 'rb'))
    
    374
    -                    for chunk in iter(lambda: tmp.read(4096), b""):
    
    376
    +                    for chunk in iter(lambda: tmp.read(_BUFFER_SIZE), b""):
    
    375 377
                             h.update(chunk)
    
    376 378
                     else:
    
    377 379
                         tmp = stack.enter_context(utils._tempnamedfile(dir=self.tmpdir))
    
    ... ... @@ -380,7 +382,7 @@ class CASCache():
    380 382
     
    
    381 383
                         if path:
    
    382 384
                             with open(path, 'rb') as f:
    
    383
    -                            for chunk in iter(lambda: f.read(4096), b""):
    
    385
    +                            for chunk in iter(lambda: f.read(_BUFFER_SIZE), b""):
    
    384 386
                                     h.update(chunk)
    
    385 387
                                     tmp.write(chunk)
    
    386 388
                         else:
    

  • buildstream/_includes.py
    ... ... @@ -40,19 +40,34 @@ class Includes:
    40 40
                 includes = [_yaml.node_get(node, str, '(@)')]
    
    41 41
             else:
    
    42 42
                 includes = _yaml.node_get(node, list, '(@)', default_value=None)
    
    43
    +
    
    44
    +        include_provenance = None
    
    43 45
             if '(@)' in node:
    
    46
    +            include_provenance = _yaml.node_get_provenance(node, key='(@)')
    
    44 47
                 del node['(@)']
    
    45 48
     
    
    46 49
             if includes:
    
    47 50
                 for include in reversed(includes):
    
    48 51
                     if only_local and ':' in include:
    
    49 52
                         continue
    
    50
    -                include_node, file_path, sub_loader = self._include_file(include,
    
    51
    -                                                                         current_loader)
    
    53
    +                try:
    
    54
    +                    include_node, file_path, sub_loader = self._include_file(include,
    
    55
    +                                                                             current_loader)
    
    56
    +                except LoadError as e:
    
    57
    +                    if e.reason == LoadErrorReason.MISSING_FILE:
    
    58
    +                        message = "{}: Include block references a file that could not be found: '{}'.".format(
    
    59
    +                            include_provenance, include)
    
    60
    +                        raise LoadError(LoadErrorReason.MISSING_FILE, message) from e
    
    61
    +                    elif e.reason == LoadErrorReason.LOADING_DIRECTORY:
    
    62
    +                        message = "{}: Include block references a directory instead of a file: '{}'.".format(
    
    63
    +                            include_provenance, include)
    
    64
    +                        raise LoadError(LoadErrorReason.LOADING_DIRECTORY, message) from e
    
    65
    +                    else:
    
    66
    +                        raise
    
    67
    +
    
    52 68
                     if file_path in included:
    
    53
    -                    provenance = _yaml.node_get_provenance(node)
    
    54 69
                         raise LoadError(LoadErrorReason.RECURSIVE_INCLUDE,
    
    55
    -                                    "{}: trying to recursively include {}". format(provenance,
    
    70
    +                                    "{}: trying to recursively include {}". format(include_provenance,
    
    56 71
                                                                                        file_path))
    
    57 72
                     # Because the included node will be modified, we need
    
    58 73
                     # to copy it so that we do not modify the toplevel
    
    ... ... @@ -101,7 +116,7 @@ class Includes:
    101 116
             file_path = os.path.join(directory, include)
    
    102 117
             key = (current_loader, file_path)
    
    103 118
             if key not in self._loaded:
    
    104
    -            self._loaded[key] = _yaml.load(os.path.join(directory, include),
    
    119
    +            self._loaded[key] = _yaml.load(file_path,
    
    105 120
                                                shortname=shortname,
    
    106 121
                                                project=project,
    
    107 122
                                                copy_tree=self._copy_tree)
    

  • buildstream/_yaml.py
    ... ... @@ -365,8 +365,8 @@ _sentinel = object()
    365 365
     #
    
    366 366
     def node_get(node, expected_type, key, indices=None, *, default_value=_sentinel, allow_none=False):
    
    367 367
         value = node.get(key, default_value)
    
    368
    -    provenance = node_get_provenance(node)
    
    369 368
         if value is _sentinel:
    
    369
    +        provenance = node_get_provenance(node)
    
    370 370
             raise LoadError(LoadErrorReason.INVALID_DATA,
    
    371 371
                             "{}: Dictionary did not contain expected key '{}'".format(provenance, key))
    
    372 372
     
    
    ... ... @@ -922,9 +922,20 @@ RoundTripRepresenter.add_representer(SanitizedDict,
    922 922
     # Only dicts are ordered, list elements are left in order.
    
    923 923
     #
    
    924 924
     def node_sanitize(node):
    
    925
    +    # Short-circuit None which occurs ca. twice per element
    
    926
    +    if node is None:
    
    927
    +        return node
    
    928
    +
    
    929
    +    node_type = type(node)
    
    930
    +    # Next short-circuit integers, floats, strings, booleans, and tuples
    
    931
    +    if node_type in (int, float, str, bool, tuple):
    
    932
    +        return node
    
    933
    +    # Now short-circuit lists
    
    934
    +    elif node_type is list:
    
    935
    +        return [node_sanitize(elt) for elt in node]
    
    925 936
     
    
    926
    -    if isinstance(node, collections.abc.Mapping):
    
    927
    -
    
    937
    +    # Finally ChainMap and dict, and other Mappings need special handling
    
    938
    +    if node_type in (dict, ChainMap) or isinstance(node, collections.Mapping):
    
    928 939
             result = SanitizedDict()
    
    929 940
     
    
    930 941
             key_list = [key for key, _ in node_items(node)]
    
    ... ... @@ -932,10 +943,10 @@ def node_sanitize(node):
    932 943
                 result[key] = node_sanitize(node[key])
    
    933 944
     
    
    934 945
             return result
    
    935
    -
    
    936 946
         elif isinstance(node, list):
    
    937 947
             return [node_sanitize(elt) for elt in node]
    
    938 948
     
    
    949
    +    # Everything else (such as commented scalars) just gets returned as-is.
    
    939 950
         return node
    
    940 951
     
    
    941 952
     
    
    ... ... @@ -1064,15 +1075,48 @@ class ChainMap(collections.ChainMap):
    1064 1075
                 return default
    
    1065 1076
     
    
    1066 1077
     
    
    1078
    +# Node copying
    
    1079
    +#
    
    1080
    +# Unfortunately we copy nodes a *lot* and `isinstance()` is super-slow when
    
    1081
    +# things from collections.abc get involved.  The result is the following
    
    1082
    +# intricate but substantially faster group of tuples and the use of `in`.
    
    1083
    +#
    
    1084
    +# If any of the {node,list}_{chain_,}_copy routines raise a ValueError
    
    1085
    +# then it's likely additional types need adding to these tuples.
    
    1086
    +
    
    1087
    +# When chaining a copy, these types are skipped since the ChainMap will
    
    1088
    +# retrieve them from the source node when needed.  Other copiers might copy
    
    1089
    +# them, so we call them __quick_types.
    
    1090
    +__quick_types = (str, bool,
    
    1091
    +                 yaml.scalarstring.PreservedScalarString,
    
    1092
    +                 yaml.scalarstring.SingleQuotedScalarString,
    
    1093
    +                 yaml.scalarstring.DoubleQuotedScalarString)
    
    1094
    +
    
    1095
    +# These types have to be iterated like a dictionary
    
    1096
    +__dict_types = (dict, ChainMap, yaml.comments.CommentedMap)
    
    1097
    +
    
    1098
    +# These types have to be iterated like a list
    
    1099
    +__list_types = (list, yaml.comments.CommentedSeq)
    
    1100
    +
    
    1101
    +# These are the provenance types, which have to be cloned rather than any other
    
    1102
    +# copying tactic.
    
    1103
    +__provenance_types = (Provenance, DictProvenance, MemberProvenance, ElementProvenance)
    
    1104
    +
    
    1105
    +
    
    1067 1106
     def node_chain_copy(source):
    
    1068 1107
         copy = ChainMap({}, source)
    
    1069 1108
         for key, value in source.items():
    
    1070
    -        if isinstance(value, collections.abc.Mapping):
    
    1109
    +        value_type = type(value)
    
    1110
    +        if value_type in __dict_types:
    
    1071 1111
                 copy[key] = node_chain_copy(value)
    
    1072
    -        elif isinstance(value, list):
    
    1112
    +        elif value_type in __list_types:
    
    1073 1113
                 copy[key] = list_chain_copy(value)
    
    1074
    -        elif isinstance(value, Provenance):
    
    1114
    +        elif value_type in __provenance_types:
    
    1075 1115
                 copy[key] = value.clone()
    
    1116
    +        elif value_type in __quick_types:
    
    1117
    +            pass  # No need to copy these, the chainmap deals with it
    
    1118
    +        else:
    
    1119
    +            raise ValueError("Unable to be quick about node_chain_copy of {}".format(value_type))
    
    1076 1120
     
    
    1077 1121
         return copy
    
    1078 1122
     
    
    ... ... @@ -1080,14 +1124,17 @@ def node_chain_copy(source):
    1080 1124
     def list_chain_copy(source):
    
    1081 1125
         copy = []
    
    1082 1126
         for item in source:
    
    1083
    -        if isinstance(item, collections.abc.Mapping):
    
    1127
    +        item_type = type(item)
    
    1128
    +        if item_type in __dict_types:
    
    1084 1129
                 copy.append(node_chain_copy(item))
    
    1085
    -        elif isinstance(item, list):
    
    1130
    +        elif item_type in __list_types:
    
    1086 1131
                 copy.append(list_chain_copy(item))
    
    1087
    -        elif isinstance(item, Provenance):
    
    1132
    +        elif item_type in __provenance_types:
    
    1088 1133
                 copy.append(item.clone())
    
    1089
    -        else:
    
    1134
    +        elif item_type in __quick_types:
    
    1090 1135
                 copy.append(item)
    
    1136
    +        else:  # Fallback
    
    1137
    +            raise ValueError("Unable to be quick about list_chain_copy of {}".format(item_type))
    
    1091 1138
     
    
    1092 1139
         return copy
    
    1093 1140
     
    
    ... ... @@ -1095,14 +1142,17 @@ def list_chain_copy(source):
    1095 1142
     def node_copy(source):
    
    1096 1143
         copy = {}
    
    1097 1144
         for key, value in source.items():
    
    1098
    -        if isinstance(value, collections.abc.Mapping):
    
    1145
    +        value_type = type(value)
    
    1146
    +        if value_type in __dict_types:
    
    1099 1147
                 copy[key] = node_copy(value)
    
    1100
    -        elif isinstance(value, list):
    
    1148
    +        elif value_type in __list_types:
    
    1101 1149
                 copy[key] = list_copy(value)
    
    1102
    -        elif isinstance(value, Provenance):
    
    1150
    +        elif value_type in __provenance_types:
    
    1103 1151
                 copy[key] = value.clone()
    
    1104
    -        else:
    
    1152
    +        elif value_type in __quick_types:
    
    1105 1153
                 copy[key] = value
    
    1154
    +        else:
    
    1155
    +            raise ValueError("Unable to be quick about node_copy of {}".format(value_type))
    
    1106 1156
     
    
    1107 1157
         ensure_provenance(copy)
    
    1108 1158
     
    
    ... ... @@ -1112,18 +1162,25 @@ def node_copy(source):
    1112 1162
     def list_copy(source):
    
    1113 1163
         copy = []
    
    1114 1164
         for item in source:
    
    1115
    -        if isinstance(item, collections.abc.Mapping):
    
    1165
    +        item_type = type(item)
    
    1166
    +        if item_type in __dict_types:
    
    1116 1167
                 copy.append(node_copy(item))
    
    1117
    -        elif isinstance(item, list):
    
    1168
    +        elif item_type in __list_types:
    
    1118 1169
                 copy.append(list_copy(item))
    
    1119
    -        elif isinstance(item, Provenance):
    
    1170
    +        elif item_type in __provenance_types:
    
    1120 1171
                 copy.append(item.clone())
    
    1121
    -        else:
    
    1172
    +        elif item_type in __quick_types:
    
    1122 1173
                 copy.append(item)
    
    1174
    +        else:
    
    1175
    +            raise ValueError("Unable to be quick about list_copy of {}".format(item_type))
    
    1123 1176
     
    
    1124 1177
         return copy
    
    1125 1178
     
    
    1126 1179
     
    
    1180
    +# Helpers for the assertions
    
    1181
    +__node_assert_composition_directives = ('(>)', '(<)', '(=)')
    
    1182
    +
    
    1183
    +
    
    1127 1184
     # node_final_assertions()
    
    1128 1185
     #
    
    1129 1186
     # This must be called on a fully loaded and composited node,
    
    ... ... @@ -1142,22 +1199,26 @@ def node_final_assertions(node):
    1142 1199
             # indicates that the user intended to override a list which
    
    1143 1200
             # never existed in the underlying data
    
    1144 1201
             #
    
    1145
    -        if key in ['(>)', '(<)', '(=)']:
    
    1202
    +        if key in __node_assert_composition_directives:
    
    1146 1203
                 provenance = node_get_provenance(node, key)
    
    1147 1204
                 raise LoadError(LoadErrorReason.TRAILING_LIST_DIRECTIVE,
    
    1148 1205
                                 "{}: Attempt to override non-existing list".format(provenance))
    
    1149 1206
     
    
    1150
    -        if isinstance(value, collections.abc.Mapping):
    
    1207
    +        value_type = type(value)
    
    1208
    +
    
    1209
    +        if value_type in __dict_types:
    
    1151 1210
                 node_final_assertions(value)
    
    1152
    -        elif isinstance(value, list):
    
    1211
    +        elif value_type in __list_types:
    
    1153 1212
                 list_final_assertions(value)
    
    1154 1213
     
    
    1155 1214
     
    
    1156 1215
     def list_final_assertions(values):
    
    1157 1216
         for value in values:
    
    1158
    -        if isinstance(value, collections.abc.Mapping):
    
    1217
    +        value_type = type(value)
    
    1218
    +
    
    1219
    +        if value_type in __dict_types:
    
    1159 1220
                 node_final_assertions(value)
    
    1160
    -        elif isinstance(value, list):
    
    1221
    +        elif value_type in __list_types:
    
    1161 1222
                 list_final_assertions(value)
    
    1162 1223
     
    
    1163 1224
     
    

  • buildstream/element.py
    ... ... @@ -2108,11 +2108,14 @@ class Element(Plugin):
    2108 2108
                 }
    
    2109 2109
     
    
    2110 2110
                 self.__cache_key_dict['fatal-warnings'] = sorted(project._fatal_warnings)
    
    2111
    +            self.__cache_key_dict['dependencies'] = []
    
    2112
    +            self.__cache_key_dict = _yaml.node_sanitize(self.__cache_key_dict)
    
    2111 2113
     
    
    2112
    -        cache_key_dict = self.__cache_key_dict.copy()
    
    2113
    -        cache_key_dict['dependencies'] = dependencies
    
    2114
    +        # This replacement is safe since OrderedDict replaces the value,
    
    2115
    +        # leaving its location in the dictionary alone.
    
    2116
    +        self.__cache_key_dict['dependencies'] = _yaml.node_sanitize(dependencies)
    
    2114 2117
     
    
    2115
    -        return _cachekey.generate_key(cache_key_dict)
    
    2118
    +        return _cachekey.generate_key_pre_sanitized(self.__cache_key_dict)
    
    2116 2119
     
    
    2117 2120
         # __can_build_incrementally()
    
    2118 2121
         #
    

  • buildstream/utils.py
    ... ... @@ -235,7 +235,7 @@ def sha256sum(filename):
    235 235
         try:
    
    236 236
             h = hashlib.sha256()
    
    237 237
             with open(filename, "rb") as f:
    
    238
    -            for chunk in iter(lambda: f.read(4096), b""):
    
    238
    +            for chunk in iter(lambda: f.read(65536), b""):
    
    239 239
                     h.update(chunk)
    
    240 240
     
    
    241 241
         except OSError as e:
    

  • tests/format/include.py
    1 1
     import os
    
    2
    +import textwrap
    
    2 3
     import pytest
    
    3 4
     from buildstream import _yaml
    
    4 5
     from buildstream._exceptions import ErrorDomain, LoadErrorReason
    
    ... ... @@ -27,6 +28,46 @@ def test_include_project_file(cli, datafiles):
    27 28
         assert loaded['included'] == 'True'
    
    28 29
     
    
    29 30
     
    
    31
    +def test_include_missing_file(cli, tmpdir):
    
    32
    +    tmpdir.join('project.conf').write('{"name": "test"}')
    
    33
    +    element = tmpdir.join('include_missing_file.bst')
    
    34
    +
    
    35
    +    # Normally we would use dicts and _yaml.dump to write such things, but here
    
    36
    +    # we want to be sure of a stable line and column number.
    
    37
    +    element.write(textwrap.dedent("""
    
    38
    +        kind: manual
    
    39
    +
    
    40
    +        "(@)":
    
    41
    +          - nosuch.yaml
    
    42
    +    """).strip())
    
    43
    +
    
    44
    +    result = cli.run(project=str(tmpdir), args=['show', str(element.basename)])
    
    45
    +    result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.MISSING_FILE)
    
    46
    +    # Make sure the root cause provenance is in the output.
    
    47
    +    assert 'line 4 column 2' in result.stderr
    
    48
    +
    
    49
    +
    
    50
    +def test_include_dir(cli, tmpdir):
    
    51
    +    tmpdir.join('project.conf').write('{"name": "test"}')
    
    52
    +    tmpdir.mkdir('subdir')
    
    53
    +    element = tmpdir.join('include_dir.bst')
    
    54
    +
    
    55
    +    # Normally we would use dicts and _yaml.dump to write such things, but here
    
    56
    +    # we want to be sure of a stable line and column number.
    
    57
    +    element.write(textwrap.dedent("""
    
    58
    +        kind: manual
    
    59
    +
    
    60
    +        "(@)":
    
    61
    +          - subdir/
    
    62
    +    """).strip())
    
    63
    +
    
    64
    +    result = cli.run(project=str(tmpdir), args=['show', str(element.basename)])
    
    65
    +    result.assert_main_error(
    
    66
    +        ErrorDomain.LOAD, LoadErrorReason.LOADING_DIRECTORY)
    
    67
    +    # Make sure the root cause provenance is in the output.
    
    68
    +    assert 'line 4 column 2' in result.stderr
    
    69
    +
    
    70
    +
    
    30 71
     @pytest.mark.datafiles(DATA_DIR)
    
    31 72
     def test_include_junction_file(cli, tmpdir, datafiles):
    
    32 73
         project = os.path.join(str(datafiles), 'junction')
    
    ... ... @@ -47,7 +88,7 @@ def test_include_junction_file(cli, tmpdir, datafiles):
    47 88
     
    
    48 89
     
    
    49 90
     @pytest.mark.datafiles(DATA_DIR)
    
    50
    -def test_include_junction_options(cli, tmpdir, datafiles):
    
    91
    +def test_include_junction_options(cli, datafiles):
    
    51 92
         project = os.path.join(str(datafiles), 'options')
    
    52 93
     
    
    53 94
         result = cli.run(project=project, args=[
    
    ... ... @@ -128,7 +169,7 @@ def test_junction_element_not_partial_project_file(cli, tmpdir, datafiles):
    128 169
     
    
    129 170
     
    
    130 171
     @pytest.mark.datafiles(DATA_DIR)
    
    131
    -def test_include_element_overrides(cli, tmpdir, datafiles):
    
    172
    +def test_include_element_overrides(cli, datafiles):
    
    132 173
         project = os.path.join(str(datafiles), 'overrides')
    
    133 174
     
    
    134 175
         result = cli.run(project=project, args=[
    
    ... ... @@ -143,7 +184,7 @@ def test_include_element_overrides(cli, tmpdir, datafiles):
    143 184
     
    
    144 185
     
    
    145 186
     @pytest.mark.datafiles(DATA_DIR)
    
    146
    -def test_include_element_overrides_composition(cli, tmpdir, datafiles):
    
    187
    +def test_include_element_overrides_composition(cli, datafiles):
    
    147 188
         project = os.path.join(str(datafiles), 'overrides')
    
    148 189
     
    
    149 190
         result = cli.run(project=project, args=[
    
    ... ... @@ -158,7 +199,7 @@ def test_include_element_overrides_composition(cli, tmpdir, datafiles):
    158 199
     
    
    159 200
     
    
    160 201
     @pytest.mark.datafiles(DATA_DIR)
    
    161
    -def test_include_element_overrides_sub_include(cli, tmpdir, datafiles):
    
    202
    +def test_include_element_overrides_sub_include(cli, datafiles):
    
    162 203
         project = os.path.join(str(datafiles), 'sub-include')
    
    163 204
     
    
    164 205
         result = cli.run(project=project, args=[
    
    ... ... @@ -192,7 +233,7 @@ def test_junction_do_not_use_included_overrides(cli, tmpdir, datafiles):
    192 233
     
    
    193 234
     
    
    194 235
     @pytest.mark.datafiles(DATA_DIR)
    
    195
    -def test_conditional_in_fragment(cli, tmpdir, datafiles):
    
    236
    +def test_conditional_in_fragment(cli, datafiles):
    
    196 237
         project = os.path.join(str(datafiles), 'conditional')
    
    197 238
     
    
    198 239
         result = cli.run(project=project, args=[
    
    ... ... @@ -222,7 +263,7 @@ def test_inner(cli, datafiles):
    222 263
     
    
    223 264
     
    
    224 265
     @pytest.mark.datafiles(DATA_DIR)
    
    225
    -def test_recusive_include(cli, tmpdir, datafiles):
    
    266
    +def test_recursive_include(cli, datafiles):
    
    226 267
         project = os.path.join(str(datafiles), 'recursive')
    
    227 268
     
    
    228 269
         result = cli.run(project=project, args=[
    
    ... ... @@ -231,6 +272,7 @@ def test_recusive_include(cli, tmpdir, datafiles):
    231 272
             '--format', '%{vars}',
    
    232 273
             'element.bst'])
    
    233 274
         result.assert_main_error(ErrorDomain.LOAD, LoadErrorReason.RECURSIVE_INCLUDE)
    
    275
    +    assert 'line 2 column 2' in result.stderr
    
    234 276
     
    
    235 277
     
    
    236 278
     @pytest.mark.datafiles(DATA_DIR)
    



  • [Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]