Архитектура Аудит Военная наука Иностранные языки Медицина Металлургия Метрология Образование Политология Производство Психология Стандартизация Технологии |
Тестирование программ. Восходящее и нисходящее тестирование.
Тестирование - это процесс исполнения программы на компьютере с целью обнаружения ошибок. Поясним это определение.
Тестом будем называть информацию, состоящую из исходных данных, специально подобранных для отлаживаемой программы, и из соответствующих им эталонных результатов (не только окончательных, но и промежуточных), используемых в дальнейшем для контроля правильности работы программы. Если поставить целью демонстрацию отсутствия ошибок, то мы подсознательно будем стремиться к этой цели, выбирая тестовые данные, на которых вероятность появления ошибки мала. В то же время, если нашей задачей станет обнаружение ошибок, то создаваемый нами тест будет обладать большей вероятностью обнаружения ошибки. Такой подход заметнее повысит качество программы.
Тестирование - процесс деструктивный (то есть обратный созидательному, конструктивному). Именно этим и объясняется, почему многие считают его трудным. Большинство людей склонно к конструктивному процессу созидания объектов и в меньшей степени - к деструктивному процессу разделения на части. Для усиления определения тестирования проанализируем два понятия - " удачный" и " неудачный". Большинство назовет тестовый прогон неудачным, если обнаружена ошибка и, наоборот, удачным, если он прошел без ошибок. Из определения тестирования следует противоположное:
Вопрос о позиции программиста по отношению к продукту его труда связан, как это показано Вейнбергом, с принципами безличного программирования и когнитивного диссонанса. Когнитивный диссонанс - это психологический принцип, который руководит действиями человека, чьи представления о себе оказались под угрозой. Программист, искренне считающий программу продолжением своего " я", не будет пытаться найти все ошибки в ней. Напротив, он постарается показать, что программа правильна, даже если там будут находиться чудовищные для постороннего взгляда ошибки. Спасти в такой ситуации может безличное программирование. Вместо того, чтобы быть скрытным и защищать свою программу, программист занимает противоположную позицию: он открыто приглашает других программистов читать и конструктивно критиковать ее. Когда кто-то находит ошибку в его программе, программист, конечно, не должен радоваться, что ошибся; его позиция примерно такова: " О! Мы нашли ошибку в нашей программе! Хорошо, что мы нашли ее сейчас, а не позже! Поучимся на этой ошибке, а заодно посмотрим, не найдем ли еще! " Программист, обнаруживший ошибку в чужой программе, не кричит: " Посмотри на свою идиотскую ошибку! ", а реагирует примерно так: " Как любопытно! Интересно, не сделал ли и я такой ошибки в написанном мною модуле? " При использовании метода тестов
Таким образом, контроль программы сводится к тому, чтобы подобрать систему тестов, получение правильных результатов для которой гарантировало бы правильную работу программы и для остальных исходных данных из области, указанной в решаемой задаче.
Для реализации метода контрольных тестов должны быть изготовлены или заранее известны эталонные результаты, на основании сверки с которыми получаемых тестовых результатов, можно было бы сделать вывод о правильности работы программы на данном тесте.
Эталонные результаты для вычислительных задач можно получить, осуществляя вычисления вручную, применяя результаты, полученные ранее на другом компьютере или по другой программе, или, используя известные факты, свойства, физические законы.
Разрабатывая систему тестов, нужно стремиться к тому, чтобы успешный пропуск ее на компьютере доказывал наличие ошибок в программе (или отдельном ее блоке), хотя для многих достаточно сложных программ, особенно если над ними работает несколько программистов, можно практически говорить лишь о большей или меньшей вероятности правильности программы. Это объясняется тем, что изготовление и пропуск всех тестов, необходимых для доказательства, может потребовать такого объема работ, который затянет этап контроля на многие месяцы или годы. Поэтому при разработке системы тестов наряду с задачей всестороннего и глубокого тестирования, стоит задача минимизации количества необходимых тестовых результатов, машинного времени и усилий программиста. В большинстве случаев при использовании метода контрольных тестов вопрос о доказательстве отсутствия ошибок практически можно ставить лишь для небольших блоков (модулей) программы, а для целой программы приходится ограничиваться той или иной вероятностью отсутствия ошибок в программе. Неоднократно экспериментально установлено, что в любой сложной программе в процессе эксплуатации обнаруживаются ошибки, даже если проведено самое тщательное тестирование. Тем самым утверждается объективная реальность, заключающаяся в невозможности формализовать и обеспечить абсолютную полноту всех эталонных значений, а также провести всеобъемлющее исчерпывающее тестирование и устранить все ошибки в сложных программах. Опыт показывает, что до начала тестирования число ошибок в сложных программах - порядка 1-2% от общего числа операторов в программе. Самое тщательное тестирование сложных программ позволяет получить программы с вероятностью ошибки в каждом операторе 0, 0001 - 0, 00001, то есть несколько ошибок может остаться. После завершения тестирования программы в течение нескольких лет эксплуатации могут быть выявлены еще десятки ошибок!
еречислим основные принципы тестирования [1]. Эти принципы интересны тем, что в основном они интуитивно ясны, но в то же время на них часто не обращают должного внимания. · Описание предполагаемых значений выходных данных или результатов должно быть необходимой частью тестового набора. Нарушение этого очевидного принципа представляет одну из наиболее распространенных ошибок. Ошибочные, но правдоподобные результаты могут быть признаны правильными, если результаты теста не были заранее определены. Здесь мы сталкиваемся с явлением психологии: мы видим то, что мы хотим увидеть. Другими словами, несмотря на то, что тестирование по определению - деструктивный процесс, есть подсознательное желание видеть корректный результат. Один из способов борьбы с этим состоит в поощрении детального анализа выходных переменных заранее, еще при разработке теста. · Следует избегать тестирования программы ее автором. Многие, кому приходилось самому делать дома ремонт, знают, что процесс обрывания старых обоев (деструктивный процесс) не легок, но он просто невыносим, если не кто-то другой, а Вы сами первоначально их наклеивали. Вот так же и большинство программистов не может эффективно тестировать свои программы, потому что им трудно демонстрировать собственные ошибки. · Необходимо досконально изучать результаты применения каждого теста. Представляется достоверным, что значительная часть всех обнаруженных в конечном итоге ошибок могла быть выявлена в результате самых первых тестовых прогонов, но они были пропущены вследствие недостаточно тщательного анализа результатов первого тестового прогона. · Тесты для неправильных и непредусмотренных входных данных следует разрабатывать так же тщательно, как для правильных и предусмотренных. Вполне вероятно, что тесты, представляющие неверные и неправильные входные данные, обладают большей обнаруживающей способностью, чем тесты, соответствующие корректным входным данным. · Необходимо проверять не только, делает ли программа то, для чего она предназначена, но и ни делает ли она то, что не должна делать. Обязательно проверяйте программу на нежелательные побочные эффекты. · Не следует выбрасывать тесты, даже если программа уже не нужна. Необходимость в использованных тестах наиболее часто возникает в интерактивных системах отладки. Обычно тестирующий сидит за терминалом, на лету придумывает тесты и запускает программу на выполнение. При такой практике работы после применения тесты пропадают. После внесения изменений или исправления ошибок необходимо повторить тестирование, тогда приходится заново изобретать тесты. Как правило, этого стараются избегать, поскольку повторное создание тестов требует значительной работы. · Нельзя планировать тестирование в предположении, что ошибки не будут обнаружены. · Вероятность наличия необнаруженных ошибок в части программы пропорциональна числу ошибок, уже обнаруженных в этой части. На первый взгляд, этот принцип лишен смысла, но тем не менее подтверждается многими программами. Например, допустим, что некоторая программа состоит из модулей А и В. К определенному сроку в модуле А обнаружено пять ошибок, а в модуле В - только одна, причем модуль А не подвергался более тщательному тестированию. Тогда из рассматриваемого принципа следует, что вероятность необнаруженных ошибок в модуле А больше, чем в модуле В. Справедливость этого принципа подтверждается еще и тем, что для ошибок свойственно располагаться в программе в виде неких скоплений, хотя данное явление пока никем еще не объяснено.
Таким образом, если в какой-нибудь части программы обнаружено больше ошибок, чем в других, то на ее тестирование должны быть направлены дополнительные усилия. Тестирование - процесс творческий. Вполне вероятно, что для тестирования большой программы требуется больший творческий потенциал, чем для ее проектирования. Чтобы подчеркнуть некоторые мысли, высказанные на этом шаге, приведем еще раз три наиболее важных принципа тестирования.
" На закуску" рекомендуем выполнить следующий простой тест. Задача состоит в том, чтобы проверить программу, которая по трем заданным числам печатает сообщение о том, является ли треугольник со сторонами, длины которых равны данным значениям, неравносторонним, равнобедренным или равносторонним [2]. Напишите на листе бумаги набор тестов, которые, как Вам кажется, будут адекватно проверять эту программу. Построив свои тесты, проанализируйте их.
Поговорим о методах тестирования. При неупорядоченном тестировании (Smoke Test - грубая проверка работоспособности простым запуском, дымовой тест) исходные данные, имитирующие внешнюю среду, случайным образом генерируются во всем диапазоне возможного изменения параметров. При этом многие значения исходных данных характеризуются малой вероятностью обнаружения ошибок и не оправдывают затраты на выполнение тестирования. Кроме того, возможно появление логически противоречивых данных. В то же время данные, наиболее важные с позиции реального использования программ и возможностей обнаружения ошибок, могут оказаться неохваченными в процессе тестирования. Поэтому на практике последовательно применяют следующие методы тестирования: · статический, · детерминированный и · стохастический.
Статическое тестирование (Static Check) является наиболее формализованным методом проверки корректности программ. Тестирование проводится без исполнения программы путем формального анализа текста программы на языке программирования. Операторы и операнды текста программ при этом анализируются в символьном виде, поэтому такой метод называют также символическим тестированием. Наиболее трудоемкими и детализирующими являются методы детерминированного тестирования. При детерминированном тестировании контролируется каждая комбинация исходных эталонных данных и соответствующая ей комбинация эталонных результатов. Разумеется, в сложных программах невозможно перебрать все комбинации исходных данных и проконтролировать результаты функционирования программы на каждой из них. В таких случаях применяется стохастическое тестирование, при котором исходные тестовые данные задаются множеством случайных величин с соответствующими распределениями и для сравнения полученных результатов используются также распределения случайных величин. В результате при стохастическом тестировании возможно более широкое варьирование исходных данных, хотя отдельные ошибки могут быть не обнаружены, если они мало искажают средние статистические значения или распределения. Стохастическое тестирование применяется в основном для обнаружения ошибок, а для диагностики и локализации ошибок приходится переходить к детерминированному тестированию с использованием конкретных значений параметров из области изменения использовавшихся случайных величин. Рассмотрим некоторые правила тестирования, в которых делается попытка учесть как желательность доказательства правильности контролируемой программы, так и ограниченность человеческих возможностей при проведении такого доказательства [1]. Проход участков. Каждый линейный участок программы должен быть обязательно пройден при выполнении, по крайней мере, одного теста. Очевидно, что в противном случае никакой гарантии в правильности работы всей программы дать будет нельзя. В том случае, когда выполнение некоторого участка программы меняет порядок выполнения или характер работы других участков, может потребоваться перебор всех ветвей программы, то есть проход по всем возможным путям выполнения программы (многократная проверка требуется, в частности, для участков, содержащих переменные с индексами).
Точность проверки. Контроль арифметических блоков (как и других блоков) производится путем сверки результатов, полученных при выполнении блока, с эталонными результатами. Для арифметических результатов дополнительная сложность заключается в определении точности, с которой необходимо сверять (и, тем самым, вычислять) эталонные и тестовые результаты, с тем, чтобы можно было действительно удостовериться в правильности работы блока. Дело в том, что величины, входящие в проверяемое арифметическое выражение в зависимости от соотношения их значений и характера производимых над ними операций, вносят различный вклад в результат. Поэтому может оказаться, что неправильно запрограммированное выражение для некоторых тестовых значений величин, входящих в него, будет иметь якобы правильное значение ввиду того, что результат неправильной операции или неверно вычисленный ранее операнд выражения не окажут почти никакого влияния на тестовое (сравниваемое) значение выражения. Например, для оператора C=A+B из того, что значение C совпало с эталонным значением, не следует, что выражение записано в программе верно, поскольку для случая, когда A> > B, замена плюса на минус не будет обнаружена, если эталонное значение C вычислено с недостаточной точностью. Кроме того, если вычисление А и B не было проверено ранее, то из правильности C нельзя сделать вывод о правильности вычисления B (для случая A> > B). Таким образом, для того, чтобы быть уверенным в том, что правильный числовой результат, полученный на компьютере, говорит о правильности программы, необходимо следить за промежуточными результатами вычислений, которые не должны выходить за определенный диапазон, устанавливаемый в зависимости от точности вычислений эталонных результатов. Выполнение такого требования может привести к необходимости многократной проверки выражения для различных диапазонов данных. Минимальность вычислений. Когда продолжительность работы контролируемой программы и, тем самым, количество вычислений и необходимых для контроля тестовых данных зависит от каких-либо параметров, то при контроле их следует выбирать такими, чтобы они минимизировали количество вычислений.
К таким параметрам, например, могут относиться: · шаг или отрезок интегрирования; · порядок матрицы или количество элементов вектора; · длина символьных строк; · точность для итерационных вычислений и т.п.
Конечно, такая инициализация не должна значительно снижать надежность контроля. Следует заметить также, что значения исходных данных нужно выбирать такими, чтобы изготовление эталонных результатов вручную было, по возможности, облегчено. Например, данные могут быть сначала взяты целочисленными или такими, чтобы при проверке выражений некоторые их слагаемые, уже проверенные ранее, обращались в нуль.
Достоверность эталонов. Нужно обратить внимание и на достоверность процесса получения эталонных результатов. По возможности они должны вычисляться не самим программистом, а кем-то другим, с тем чтобы одни и те же ошибки в понимании задания не проникли и в программу, и в эталонные результаты. Если тесты готовит сам программист, то эталоны нужно вычислять до получения на компьютере соответствующих результатов. В противном случае имеется опасность невольной подгонки вычисляемых значений под желаемые, полученные ранее на компьютере. В качестве эталонных результатов часто используют и данные, полученные при ручной прокрутке программы.
Планирование. При отсутствии планового подхода тестирование обычно сводится к тому, что программист берет какие-то, можно сказать, первые попавшиеся исходные данные и пропускает программу многократно, исправляя ее при обнаружении ошибок и добиваясь того, чтобы получаемые результаты походили на желаемые. Ясно, что при этом контролируется только некоторая часть блоков и операторов, а остальные выполнятся в первый раз уже во время счета, и будут ли при этом найдены ошибки, имеющиеся в них, зависит только от случая.
При плановом подходе программа проверяется последовательно блок за блоком, причем если программа состоит из центрального блока, который проводит обращения к периферийным блокам, мало связанным друг с другом, то возможны следующие два основных подхода к контролю такой программы, два основных направления тестирования: · от периферии к центру (восходящее тестирование) или, наоборот, · от центра к периферии (нисходящее тестирование).
При первом, восходящем тестировании, применяемом обычно для небольших программ, сначала тестируют отдельные периферийные блоки, а затем переходят к тестированию центральной части, которая, разумеется, взаимодействует только с отлаженными уже блоками.
При нисходящем тестировании, используемом для достаточно больших программ, параллельно с контролем периферийных блоков (или даже до начала их контроля) производится и контроль центрального блока, выполняемого на компьютере совместно с имитаторами периферийных блоков, называемых заглушками. В задачу имитаторов входит моделирование работы соответствующих блоков с целью поддержать функционирование центрального блока. Обычно заглушки выдают простейший результат, например константу и сообщение о факте своего участия в работе. Вместо постоянной величины на наиболее поздней стадии тестирования может выдаваться и случайная величина в требуемом диапазоне.
Например, для начального контроля программы, включающей в качестве одного из своих блоков вычисление определенного интеграла, заглушка такого блока может возвращать константу. В свою очередь, блок интегрирования сам имеет периферийный блок вычисления значений подинтегральной функции, в качестве заглушки которого поначалу также может быть взята константа или простейшая функциональная зависимость.
К сожалению, часто неверно понимают функции, выполняемые заглушками. Так, порой можно услышать, что заглушка должна выполнять лишь запись сообщения, устанавливающего: " Модуль подключился". В большинстве случаев эти утверждения ошибочны. Когда модуль A вызывает модуль B, A предполагает, что B выполняет некую работу, то есть модуль A получает результаты работы модуля B. Когда же модуль B просто возвращает управление или выдает некоторое сообщение без передачи в A определенных осмысленных результатов, модуль A работает неверно не вследствие ошибок в самом модуле, а из-за несоответствия ему модуля-заглушки. Более того, результат может оказаться неудовлетворительным, если " ответ" модуля-заглушки не меняется в зависимости от условий теста. Если заглушка всегда возвращает один и тот же фиксированный результат вместо конкретного значения, предполагаемого вызывающим модулем именно в этом вызове, то вызывающий модуль сработает как ошибочный (например, зациклится) или выдаст неверное выходное значение. Следовательно,
Практически, оба этих способа редко используются в чистом виде, отдельно один от другого. Обычно ко времени, когда приступают к контролю центрального блока, какие-то простейшие периферийные блоки уже отлажены автономно, и нет необходимости моделировать их работу и разрабатывать заглушки.
Преимуществом ранней отладки центрального блока при нисходящем тестировании является то, что программист быстро получает возможность проверить периферийные блоки в условиях, которые в необходимой степени приближены к реальным. Действительно, центральный блок, снабженный хотя бы и простейшими функциональными возможностями, можно рассматривать как реальную среду, в которую " погружаются" отлаживаемые блоки, добавляемые к центральной части. Добавление отлаживаемых блоков удобно производить по одному для быстрейшего поиска ошибок, возникающих при стыковке с центральным блоком. Подключение каждого нового блока к центральной части позволяет постепенно усложнять испытания, которым подвергается тестируемая программа. Строгой, корректной процедуры подключения очередного последовательно тестируемого модуля не существует. Единственное правило, которым следует руководствоваться при выборе очередного модуля, состоит в том, что им должен быть один из модулей, вызываемых модулем, предварительно прошедшим тестирование. Запомните, что даже если изменения вносятся только в одну подпрограмму, то повторному тестированию подлежит вся система. Этот процесс называется тестированием с возвратом. Проверять работу только измененной подпрограммы недостаточно! Недостаточно полное тестирование такого рода повышает вероятность неудач.
Проведем сравнение нисходящего и восходящего тестирования [1].
Значительное повышение корректности и надежности программ достигается применением двойного или N-кратного программирования (Duplication Check - двойной просчет, двойная проверка). При этом методе при разных алгоритмах и на разных языках программирования создается несколько вариантов программы. Эти варианты реализуют одни и те же функции и при определенных тестовых данных должны выдавать тождественные результаты. Различие результатов при тестировании указывает на наличие ошибок, по крайней мере, в одном из вариантов. Обычно при разработке вариантов программы используется один и тот же алгоритм, но программы создаются на разных языках, разных компьютерах и разными программистами. На практике применяется программирование с N=2. Практически очень редки случаи, когда реальная программа создавалась в трех и более вариантах. В заключение заметим, что если исполнение теста приносит результаты, не соответствующие предполагаемым, то это означает, что: либо модуль имеет ошибку; либо неверны предполагаемые результаты (ошибки в тесте). Для устранения такого рода недоразумений нужно тщательно проверять набор тестов (" тестировать" тесты). Основным тестом мы будем называть тест, проверяющий основные функциональные возможности программы. Однако существует опасность, что после успешного окончания основного тестирования " на радостях" обычно забывают о необходимости дальнейшего, более тщательного контроля программы и отдельных ее участков, да и настроиться на такой контроль становится уже психологически трудно. Поэтому помимо основного теста необходимо применить следуюшие типы тестов. Вырожденный тест. Этот тест затрагивает работу отлаживаемой программы в самой минимальной степени. Обычно тест служит для проверки правильности выполнения самых внешних функций программы, например обращения к ней и выхода из нее. Тест граничных значений, или " стрессовый тест" (High-Low Bias Checking, Twin Check). Тест проверяет работу программы для граничных значений параметров, определяющих вычислительный процесс. Часто для граничных значений параметра работа программы носит особый характер, который тем самым требует и особого контроля. Если в качестве примера рассмотреть тестирование подпрограммы сортировки, то нужно исследовать следующие ситуации: сортируемый массив пуст; сортируемый массив содержит только один элемент; все элементы в сортируемом массиве одинаковы; массив уже отсортирован.
Л.Питер приводит следующий поучительный пример. Компьютер одной компании по страхованию автомобилей выслал проживающему в Сент-Луисе клиенту счет на сумму 0.00 долларов. Когда же компьютер направил ему " последнее уведомление" с угрозой расторгнуть договор, этот челевек обратился за помощью к своему финансовому агенту. Тот пришел к выводу, что лучший способ уладить дело - отправить компьютеру чек на 0.00 долларов. Это было сделано, и в ответ пришло подтверждение с благодарностью и заверением, что договор остается в силе!
Аварийный тест. Тест проверяет реакцию программы на возникновение разного рода аварийных ситуаций в программе, в частности вызванных неправильными исходными данными, то есть проверяется диагностика, выдаваемая программой, а также окончание ее работы или, может быть, попытка исправления неверных исходных данных (разработчики реальных программ знают, что пользователи подобны шаловливому ребенку, играющему в отсутствие старших с телевизором или магнитофоном). Поэтому в реальных программах, спроектированных с достаточной надежностью, совокупности приказов, которые должны работать только в особых аварийных ситуациях, занимают порой более 90% общего объема программы. Эти совокупности приказов называют иногда блоками защиты от дурака (Fool Proof). Такие системы, обладая достаточной надежностью, устойчиво функционируют даже при самых неподходящих действиях работающих с ними людей. В журнале " Компьютерный мир" рассказывалось об одной довольно дорого обошедшейся ошибке. При вводе данных палец оператора случайно задел не ту клавишу, и автомобиль " Форд", принадлежащий одному из граждан, стал стоить не 950, а 7000950 долларов! А если обладаешь таким дорогим имуществом, нужно платить большой налог. Налог составил 290000 долларов. Когда ошибка обнаружилась, эта сумма была уже включена в бюджет города. Владелец автомобиля получил счет на 290 тыс.долларов, но платить не стал. Причиной ошибки следует считать, конечно, не неверно нажатую клавишу, а плохую программу, автор которой не позаботился о достаточно мощных процедурах контроля входных данных. Процветающие фирмы, занятые разработкой программного обеспечения, специально нанимают профессионально неподготовленных людей, чтобы они поработали с вновь созданными программами. В их задачу входит за короткое время сделать столько неправильных обращений к программе, сколько пользователь не сделает и за долгий период. Например, когда программа запрашивает цену товара, оператор набирает на клавиатуре слово " Почему? " вместо числа и т.д. Одним из свойств хорошей программы является, как говорят специалисты, ее дружественность. Это означает, что в случае ошибки пользователя программа выдаст на экран сообщение, направленное на оказание помощи в выполнении поставленной задачи. Это может быть подсказка, наводящий вопрос, разъяснение противоречивости или иной ошибки в требованиях пользователя. В лучших образцах таких программ вместо сообщения " треугольника с такими сторонами не бывает" на экране выдаются тексты вида: " Вероятно, Вы ошиблись. На плоскости невозможно построить треугольник со сторонами, имеющими длины 1, 1, 100. Попытайтесь изменить значения длин сторон."
Существуют программы, которые не только обнаруживают, но и исправляют ошибки. Например, при проектировании какого-то прибора инженер за дисплеем подбирает параметры его деталей и вводит приказ запомнить величину сопротивления 150 кОм. Тогда компьютер может ответить: " Вероятно, Вы ошиблись. К сожалению, известны только данные о выпускаемых сопротивлениях с номиналами 160 и 180 кОм. Попытайтесь изменить значение номинала сопротивления. Если Вам подходит значение 180 кОм, нажмите клавишу " ВВОД". Программа лишь предложила один из возможных вариантов взамен явно неосуществимого. Окончательное решение осталось за пользователем. Однако дружественность программ должна иметь четкие границы, иначе автоматическое исправление ошибок превратится в медвежью услугу пользователю. Одно из свойств хороших программ состоит в том, что пользователь не должен при работе с ними удивляться, они не должны делать ничего неожиданного, так как эти неожиданности редко бывают приятными и полезными. В связи с этим интересны рекомендации по проектированию программ ведения диалога[1], где автор вообще выступает против какого-либо очеловечивания вычислительных систем. При создании вычислительных систем, которые будут вести себя как инструменты, следует избегать построения диалога по следующему принципу: " Привет, я Бетси 307, назови свое имя". Старайтесь не давать человеческих имен или признаков программам и системам. Не приписывайте вычислительным системам свободы воли или поведения, напоминающего живое существо. Если что-то происходит неправильно, не обвиняйте в этом ЭВМ или программу. Помните, что инструменты не делают ошибок, они или отказывают или ломаются.
Популярное:
|
Последнее изменение этой страницы: 2016-04-10; Просмотров: 875; Нарушение авторского права страницы